티스토리 뷰

반응형

마틴 파울러 > Mocks Aren't Stubs 글 내용이 좋아서 기록합니다.

제가 편한 말(?) 로 바꾼 부분이 많아서 원글을 읽어보시길 추천드려요!


# Regular Tests

아래는 일반적인 JUnit 테스트입니다.

 

스펙은 아래와 같습니다.

주문을 처리할 만큼의 상품이 창고에 있으면 주문이 완료되고 창고의 상품 수량은 해당 수량만큼 감소합니다. 
반면 창고에 제품이 충분하지 않으면 주문이 채워지지 않고 창고에 아무 일도 일어나지 않습니다.

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));
  }
}

 

우리가 테스트하려고 포커싱하는 클래스는 Order 이지만, Order.fill 을 위해서는 Warehouse 인스턴스가 필요합니다.

이런 상황에서 Order를 SUT, warehouse 를 Collaborator 라고 부릅니다. 

 

우리가 Order 테스트에 Warehouse를 필요로 하는 이유를 생각해보면 두가지 입니다.

1. Order.fill 이 warehouse의 메소드를 콜하는 지 테스트하고 싶다.  //  행동 검증 (behavior verification)

2. Order.fill 의 결과가 warehouse 의 상태를 바꾸는 지 테스트하고 싶다.  // 상태 검증 (state verification)

 

위에 작성한 테스트 스타일은 2번 (state verification) 입니다.

sut와 collaborators 의 State 를 검사해서 Order.fill이 잘 동작하는 지 검증하고 있기 때문입니다. 

 

 

# Tests with Mock Objects

 

위의 테스트는 실제 사용하는 Warehouse 인스턴스를 그대로 사용해서 상태검증을 하고 있습니다.

이번에는 Mock을 사용해서 행동 검증을 해보겠습니다. 

 

java mock object library인 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());
  }
}

 

첫번째 테스트 메소드 부터 살펴봅시다. 

setup 부분이 달라진 것을 볼 수 있습니다.

setup 부분이 data 와 expectations 부분으로 나뉘었습니다. 

 

data 부분

SUT는 동일하지만 collaborator 가 warehouse object 가 아니라 mock warehouse (Mock 클래스의 인스턴스) 입니다. 

 

expectations 부분

- SUT이 실행될 때 Mock의 어떤 메소드들이 불려야하는 지 예상하는 코드가 들어갔습니다. 

 

warehouse와의 interaction을 검증하는 방법이 

state verification이 아니라 behavior verification 인 것을 알 수 있습니다.

 

즉 위의 regular test 처럼 warehouse의 state를 검증하지 않고

warehouse한테 적절한 call이 발생했는 지 검증하고 있는 것이죠! 

 

 

두번째 테스트 메소드는  jMock library 를 더 활용해서 더 간단하게 작성한 버전이라고 합니다. 

 

 

jMock 뿐만 아니라 EasyMock 같은 다른 Mock 라이브러리들을 사용해도 됩니다. 

아래는 EasyMock 을 사용한 코드 입니다. 

public class OrderEasyTester extends TestCase {
  private static String TALISKER = "Talisker";
  
  private MockControl warehouseControl;
  private Warehouse warehouseMock;
  
  public void setUp() {
    warehouseControl = MockControl.createControl(Warehouse.class);
    warehouseMock = (Warehouse) warehouseControl.getMock();    
  }

  public void testFillingRemovesInventoryIfInStock() {
    //setup - data
    Order order = new Order(TALISKER, 50);
    
    //setup - expectations
    warehouseMock.hasInventory(TALISKER, 50);
    warehouseControl.setReturnValue(true);
    warehouseMock.remove(TALISKER, 50);
    warehouseControl.replay();

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

  public void testFillingDoesNotRemoveIfNotEnoughInStock() {
    Order order = new Order(TALISKER, 51);    

    warehouseMock.hasInventory(TALISKER, 51);
    warehouseControl.setReturnValue(false);
    warehouseControl.replay();

    order.fill((Warehouse) warehouseMock);

    assertFalse(order.isFilled());
    warehouseControl.verify();
  }
}

 

 

# The Difference Between Mocks and Stubs

이번에는 다른 예제를 통해 real object가 아닌 가짜 object로 상태 검증하는 테스트코드를 살펴봅시다. 

(메일 서비스 같은 경우, real object = real mail service 로 협력하면 문제가 있기 때문입니다.)

 

위에서 행동 검증을 위해 쓰이는 가짜 Object를 Mock이라고 지칭했다면,

상태 검증을 위해 쓰이는 가짜 object 는 Stub 이라고 불립니다. 

그래서 MailServiceStub 으로 네이밍을 해줍니다. 

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();
  }
}                                 

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());
  }

 

만약에 Mock을 사용한다면 테스트는 이런 모양이여야할 것 입니다. 

class OrderInteractionTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

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

 

 

# Test doubles 용어 정리 

Test Double은 테스트를 위해 real object를 대신하여 사용되는 가짜 object를 모두 지칭하는 일반적인 용어입니다.

위에서 살펴본 Mock, Stub 은 Test Dobule의 한 종류라고 할 수 있습니다. 

이 뿐만 아니라 Spy, Dummy, Fake 도 Test Dobule의 한 종류 입니다. 

 

 

1. Mock - 행동 검증

2. Stub  - 상태 검증

3. Spy - 행동 검증도 사용하는 Stub (Stub 이지만 어떻게 call 되었는 지에 대한 정보도 기록하는 Stub)

 

4. Dummy - 전달되지만 실제로 사용되지는 않음. 일반적으로 매개 변수 목록을 채우는 데만 사용됨.

5. Fake  - 실제로 동작하는 구현이 있지만, production에 적합하지 않게 간단하게 구현되어있음. (ex. in memory database)

 

 

# 상태 검증 vs 행동 검증 

상태 vs 행동 검증은 큰 결정이 아니라고 합니다. 

진짜 문제는 classical TDD style vs mockist TDD style 라고 합니다. 

 

classical TDD style

가능하다면 항상 real object를 사용하고 real object를 사용하기 어려운 경우에만 test double을 사용한다.

예를들어 real warehouse 를 사용하고 mail service는 test double을 사용한다.

test double 의 종류는 중요하지 않다. 

 

mockist TDD style 

흥미로운 행동을 가진 object에 대해 항상 Mock을 사용한다.

예를들어 warehouse, mail service 모두 Mock을 사용한다. 

이 스타일의 파생물은 BDD (행동주도개발) 이다. 

 

 

이 두 스타일에 대해 이야기 하는 부분은 원글을 읽어봐주세요 :-) 

 

 

 

 

 

 

 

 

반응형
댓글