본문 바로가기

학자형 개발

Mocks Aren't Stubs! Mock과 Stub 에대하여

class OrderStateTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }

마팅 파울러 아저씨의 Mocks Aren't Stubs 기준으로 엉덩이로 정리해봤습니다. (영어 못해요) 맨 아래 원문 사이트를 참고해주세요

 Mock 객체가 어떻게 동작하는지, 동작 검증을 기반으로 테스트를 이끄는 방법, Mock 객체를 이용해 다른 스타일의 테스트와 함께 Mock객체 주변과 커뮤니케이션하는 방법을 설명한다.

Mock과 Stub의 테스트를 확인하는 방식에 차이가 있다. Mock은 행위를 Stub은 상태를 확인하는 차이가 있다.

반면에 이 둘은 테스트와 디자인이 함께 동작하는 방식에서 완전히 다른 철학이 있다.

Regular Tests : 정규 테스트

예제는 주문 객체를 하나 생성하고, 창고 객체에 채우고 싶다.

아주 단순히 하나의 상품과 하나의 개수로 주문을 한다. 창고 객체는 상품별 인벤토리를 갖는다. 

주문을 요청하고 창고에 채우기를 요청했을 때, 두 가지 응답이 있다. 

만약 창고에 주문을 위한 상품이 충분하다면, 이 주문은 채워지고, 창고의 상품 양은 요청한 양만큼 감소한다.

창고에 주문을 위한  상품이 불충분하다면, 창고에서는 아무 일도 일어나지 않는다.

public class OrderStateTester extends TestCase {
  private static String TALISKER = "Talisker";
  private static String HIGHLAND_PARK = "Highland Park";
  private Warehouse warehouse = new WarehouseImpl();

  protected void setUp() throws Exception {
    warehouse.add(TALISKER, 50);
    warehouse.add(HIGHLAND_PARK, 25);
  }
  public void testOrderIsFilledIfEnoughInWarehouse() {
    Order order = new Order(TALISKER, 50);
    order.fill(warehouse);
    assertTrue(order.isFilled());
    assertEquals(0, warehouse.getInventory(TALISKER));
  }
  public void testOrderDoesNotRemoveIfNotEnough() {
    Order order = new Order(TALISKER, 51);
    order.fill(warehouse);
    assertFalse(order.isFilled());
    assertEquals(50, warehouse.getInventory(TALISKER));
  }

 

xUnit 테스트는 네 가지 구문을 따른다. : setup, exercise, verify, teardown.

이 예제에서는 setUp 메서드에서 setup구문을 사용해 창고의 제고와 부분적으로 주문을 세팅했다.

주문을 호출은 fill 메서드는 excercise 구문입니다. 이는 우리가 원하는 테스트를 실행하기 위해 해당 객체가 찔러진다.

setup을 하는 동안 우리가 테스트하는 객체는 Order이지만, 우리가 함께 지정한 두 가지 객체가 있다.

fill메서드가 동작하기 위해선 Warehouse 객체가 필요하다.

이런 상황에서, Order는 테스트에서 집중해야 할 객체이다.

Meszaros를 따라 System Under Test 줄여서 SUT이라고 사용하겠다.

 자, 이 테스트를 위해 나는 SUT(Order)가 필요하고, 하나의 collaborator(Warehouse)가 필요하다.

Warehouse가 필요한 두 가지 이유는 첫 번째로, 전체 Oder의 동작에서 테스트된 행위를 갖기 위함이다.

두 번 째로, 검증(한 주문의 결과. 채움은 창고의 상태를 변경하기 위한 잠재적인 변화이다)을 위해 필요하다.

이 주제를 더 탐색하면, 우리는 SUT과 collaborator사이의 거리를 더 만들 수 있다!

이런 테스팅 스타일은 state verification을 사용한다. state verfication은 실행(exercised) 메서드가 SUT와 그의 collaborator가 메서드가 실행된 후 상태를 시험하면서 정확하게 동작하는지 결정한다. 는 것을 의미한다.

Tests with Mock Objects

이제, 같은 행위와, mock 객체를 사용한 것이다. jMock을 사용하러 가즈아!

public class OrderInteractionTester extends MockObjectTestCase {
  private static String TALISKER = "Talisker";

  public void testFillingRemovesInventoryIfInStock() {
    //setup - data
    Order order = new Order(TALISKER, 50);
    Mock warehouseMock = new Mock(Warehouse.class);
    
    //setup - expectations
    warehouseMock.expects(once()).method("hasInventory")
      .with(eq(TALISKER),eq(50))
      .will(returnValue(true));
    warehouseMock.expects(once()).method("remove")
      .with(eq(TALISKER), eq(50))
      .after("hasInventory");

    //exercise
    order.fill((Warehouse) warehouseMock.proxy());
    
    //verify
    warehouseMock.verify();
    assertTrue(order.isFilled());
  }

  public void testFillingDoesNotRemoveIfNotEnoughInStock() {
    Order order = new Order(TALISKER, 51);    
    Mock warehouse = mock(Warehouse.class);
      
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());

    assertFalse(order.isFilled());
  }

 

... 아쒸 너무 길다.. 스토리는 참고사이트에 가서 보세요.

 

Mock과 Stub의 차이점

사람들이 mock을 사용할 때, mock과 다른 테스트 대역들을 완전히 이해하는 것은 중요하다.

우리는 테스트를 할 때, 한 번에 한 단위의 소프트웨어에 초점을 둔다. 그래서 유닛 테스트라는 용어로 일반적인 단어이다.

문제는 종종 다른 유닛이 필요한 하나의 유닛이 동작하도록 만드는 것이다. 그래서 우리의 예제에서는 저장소 같은 것이 필요하다.

Mezaros 테스트 대역이라는 용어를 쓴다. 테스트 대역이란 테스트 목적을 위한 실제 객체를 대신 사용되는 가상 객체의 한 종류를 칭하는 일반적인 단어이다.

Mezaros는 다섯 개의 대역의 종류를 정의한다.

  1. Dummy : 객체는 전달되지만, 실제로 사용하지 않는다. 종종 파라미터 리스트를 채우기 위해 사용된다

  2. Fake : 동작의 구현을 가지고 있지만, 프로덕션에 적합하지 않은 간결한음 제공한다.
                 (예를 들어 인메모리 데이터베이스가 좋은 예이다)

  3. Stubs : 테스트 진행 중에 호출에 대한 미리 준비된 응답을 제공하고, 준비된 응답 이외(일반적인 테스트를 위한 프로그래밍 구현)의 항목에는 전혀 응답하지 않는다.

  4. Spies :  어떻게 호출됐는지에 따라 특정 정보를 표현하는 스파이도 스텁이다. 

  5. Mocks : 기대된 호출의 구체적인 형식을 기대하는 미리 프로그래밍된 객체이다.

 

이제 Mock과 Stub의 차이를 보자. 메일링 행위를 stub를 예를 들어보자

public interface MailService {
  public void send (Message msg);
}
public class MailServiceStub implements MailService {
  private List<Message> messages = new ArrayList<Message>();
  public void send (Message msg) {
    messages.add(msg);
  }
  public int numberSent() {
    return messages.size();
  }
}                                 

stub을 이용해 상태 검증을 사용할 수 있다.

class OrderStateTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }

 

참고 사이트

- 마틴파울러 킹 사이트 :  https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs

 

Mocks Aren't Stubs

Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.

martinfowler.com