Spring framework로 개발을 할 때, 로직에 대한 Testing은 매우 중요하다.
처음 Test Code를 짤 때, Test Double에 대한 개념이 없어, 전부 다 mock으로 처리하면 되는 건지, 어떤 걸 Spy 객체로 해야하는지 모호했었다.
대부분 mock 객체만을 사용했었으나, 다양한 로직에 대한 Test code를 잘 짜기 위해 마틴 파울러의 Test Double Article
을 읽어, Test Double과 Mocks Stubs 간의 차이를 정리해본다.
마틴 파울러의 Test Double Article
Mocks Aren't Stubs
Article에서 그는 Xunit Framework를 사용한 capture pattern 중 그가 생각하기에 중요한 5가지 용어를 정리했다.
- Dummy :
- objects are passed around but never actually used. Usually they are just used to fill parameter lists. ->
실제로 사용되지 않고, 파라미터 list만 채우는데 사용.
- Fake :
- objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example). ->
실제로 구현된 동작을 실행하나, 프로덕션으로 사용하기엔 맞지 않는 shortcut(축약)된 객체들
- Stubs :
- provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. ->
테스트에 프로그램된(정의, 구현된) 내용 이외의 것에는 응답하지 않는 미리 설정된 응답을 제공한다.
- Spies :
- are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent. ->
호출 방식에 따라 몇몇 정보를 기록하는 Stub. 메시지를 몇 번 전송했는지 기록하는 email Service 같은 형태
- Mocks :
- are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting. ->
Mock은 호출될 것을 구현하여 예상되는 값을 미리 프로그래밍하는 것이다. 예상하지 않은 형태로 호출될 경우 exception을 throwing하고, 예측대로 잘 호출이 됬는지 검증한다.
...
This difference is actually two separate differences. On the one hand there is a difference in how test results are verified: a distinction betweenstate verification
andbehavior verification
. On the other hand is a whole different philosophy to the way testing and design play together, which I term here as the classical and mockist styles of Test Driven Development.
warehouse(창고) : 물류를 저장하는 창고 객체
product : 상품
inventory : 창고에 저장된 상품(product)와 그 양(quantity)
Order(주문) : warehouse에 해당 상품이 있으면, 꺼내서 주문을 채우고, warehouse에서 해당 크기 만큼 뺀다.
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);
}
// 충분한 물건이 warehouse에 있을 때 Order를 채우고, 채운만큼 warehouse에서는 빠진다.
public void testOrderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0, warehouse.getInventory(TALISKER));
}
// 갯수가 모자라면 Order를 채우지 못하고, TALISKER가 warehouse에 그대로 남아있다.
public void testOrderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50, warehouse.getInventory(TALISKER));
}
This style of testing uses
state verification
: which means that we determine whether the exercised method worked correctly by examining the state of the SUT and its collaborators after the method was exercised.
-> 이런 스타일의 테스팅은상태 검증 (: method가 수행된 이후 상태가 올바른지 시험한다)
을 이용한다.SUT
: System Under Tests 테스트 대상 시스템
jmock을 이용한 mock 객체 테스팅
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") //hasInventory 호출 setup
.with(eq(TALISKER),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove") //remove 호출 setup
.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());
}
Mocks use
behavior verification
, where we instead check to see if the order made the correct calls on the warehouse. We do this check by telling the mock what to expect during setup and asking the mock to verify itself during verification. Only the order is checked using asserts, and if the method doesn't change the state of the order there's no asserts at all.
-> Mocks는행동 검증 (: order가 warehouse에서 올바른 call을 만들었는지 체크)
을 이용한다. order를 asserts로만 체크하고, 메소드가 order의 상태를 변경하지 않았는지, asserts는 없었는지 체크한다.
The big issue here is
when to use a mock
(or other double).
마틴 파울러는 Classical test와 Mockist의 차이는 언제 Mock을 사용하는가에 있다고 한다.
The classical TDD
style is to use real objects if possible and a double if it's awkward to use the real thing. So a classical TDDer would use a real warehouse and a double for the mail service. The kind of double doesn't really matter that much.
-> Classical TDDer는 실제 객체를 사용하고, 실제 객체를 사용하기 어색한(애매한) 부분에만 test double을 사용한다. 위의 warehouse 예제에서 warehouse 객체는 실제를 사용하고, mailservice는 test double을 사용한다.
A mockist TDD
practitioner, however, will always use a mock for any object with interesting behavior. In this case for both the warehouse and the mail service.
-> mockist는 항상 mock 객체를 사용한다. 이 경우에는 warehouse와 mail service 모두 mock 객체로 수행한다.