Mocks Aren't Stubs 번역
원문은 http://martinfowler.com/articles/mocksArentStubs.html
모의객체는 스텁이 아니다(Mocks Aren't Stubs)
The term 'Mock Objects' has become a popular one to describe special case objects that mimic real objects for testing. Most language environments now have frameworks that make it easy to create mock objects. What's often not realized, however, is that mock objects are but one form of special case test object, one that enables a different style of testing. In this article I'll explain how mock objects work, how they encourage testing based on behavior verification, and how the community around them uses them to develop a different style of testing.
테스팅할 때에 진짜 객체를 흉내내는 특수한 경우의 객체를 지칭하는 것으로서 '모의객체'라는 단어가 점점 인기를 얻고 있다. 현재 대부분의 언어가 모의객체를 생성하기 쉽게 해 주는 프레임워크를 갖추고 있다. 그러나 종종 간과되는 점은, 모의객체가 특수한 테스트 객체의 한가지일 뿐만 아니라, 다른 스타일의 테스팅을 가능하게 해 준다는 점이다. 이 아티클에서 나는 모의객체가 어떻게 동작하는지, 어떻게 행위검증(behavior verification)에 기반한 테스팅을 가능하게 하는지, 그리고 모의객체를 둘러싼 커뮤니티가 어떻게 다른 스타일의 테스팅을 만드는데에 이것을 사용하는지 설명하겠다.
Last significant update: 02 Jan 07
| French | Italian | Spanish | Portuguese |
Contents
- Regular Tests
- The Difference Between Mocks and Stubs
- Classical and Mockist Testing
- So should I be a classicist or a mockist?
- Final Thoughts
I first came across the term "mock object" a few years ago in the XP community. Since then I've run into mock objects more and more. Partly this is because many of the leading developers of mock objects have been colleagues of mine at ThoughtWorks at various times. Partly it's because I see them more and more in the XP-influenced testing literature.
나는 몇년전 XP 커뮤니티에서 "모의객체"라는 용어를 처음 들었다. 그 때 이후로 나는 더 자주 모의객체를 접하게 되었다. 이렇게 된 데에는 부분적으로는 많은 모의객체 선도 개발자들이 많은 경우 소트웍스의 내 동료이기 때문이기도 하고, 또 부분적으로는 XP의 영향을 받은 많은 테스팅 저술에서 더 자주 접하게 되었기 때문이다.
But as often as not I see mock objects described poorly. In particular I see them often confused with stubs - a common helper to testing environments. I understand this confusion - I saw them as similar for a while too, but conversations with the mock developers have steadily allowed a little mock understanding to penetrate my tortoiseshell cranium.
그러나 모의객체가 빈약하게 설명되는 경우를 자주 보게 된다. 특히 스텁 - 테스팅 환경에서는 보편적으로 사용되는 도움 객체 - 과 자주 혼동되는 것을 본다. 나는 이런 혼동을 이해한다 - 나 역시 한동안 비슷한 것으로 보았다. 그러나 모의객체 개발자들과 대화하면서 나는 점차 내 거북딱지 두개골을 관통할 정도까지는 모의객체에 대해 조금 이해하게 되었다.
This difference is actually two separate differences. On the one hand there is a difference in how test results are verified: a distinction between state verification and behavior 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.
이 차이점은 사실 두 개의 구분되는 차이점이다. 한가지는 테스트 결과가 검증되는 방식의 차이 - 상태 검증(state verification)과 행위 검증(behavior verification)의 구분 - 이다. 다른 하나는 테스팅과 설계가 같이 어우러지는 방식에 대한 전혀 다른 철학의 차이이다. 이것을 여기서 나는 TDD의 고전적 스타일과 모의객체 스타일이라고 부른다.
(In the earlier version of this essay I had realized there was a difference, but combined the two differences together. Since then my understanding has improved, and as a result it's time to update this essay. If you haven't read the previous essay you can ignore my growing pains, I've written this essay as if the old version doesn't exist. But if you are familiar with the old version you may find it helpful to note that I've broken the old dichotomy of state based testing and interaction based testing into the state/behavior verification dichotomy and the classical/mockist TDD dichotomy. I've also adjusted my vocabulary to match that of the Gerard Meszaros's xUnit patterns book.)
(이 에세이의 이전 버전에서 나는 이 차이가 있음을 깨닫고 있었으나 그 두 차이를 결합했었다. 그 이후로 내 이해도는 향상되었고, 그 결과로 이 에세이를 업데이트해야 할 시간이 되었다. 이전 에세이를 읽지 않았다면 내 고민의 성장에 대해서는 무시해도 된다. 나는 이 에세이를 이전 버전이 없다고 생각하고 썼다. 이전 버전을 본 적 있다면 예전의 상태기반/행위기반 테스팅의 구분을 상태/행위 검증과 고전적/모의객체 TDD로 나누었음을 알아두는 것이 도움이 될 수도 있겠다. 또, 나는 용어를 Gerard Meszaros의 xUnit patterns book에 따라 조정도 했다.)
일반적인 테스트 (Regular Tests)
I'll begin by illustrating the two styles with a simple example. (The example is in Java, but the principles make sense with any object-oriented language.) We want to take an order object and fill it from a warehouse object. The order is very simple, with only one product and a quantity. The warehouse holds inventories of different products. When we ask an order to fill itself from a warehouse there are two possible responses. If there's enough product in the warehouse to fill the order, the order becomes filled and the warehouse's amount of the product is reduced by the appropriate amount. If there isn't enough product in the warehouse then the order isn't filled and nothing happens in the warehouse.
간단한 예를 가지고 두가지 스타일을 보여주는 것으로 시작하려 한다. (예제는 자바로 작성되었으나 어떤 객체지향 언어에서도 원칙은 통할 것이다.) 우리는 order 객체를 가지고, 이것을 warehouse 객체로부터 가져와서 채우려(fill)한다. order는 하나의 product와 quantity로 구성된 것으로서 매우 간단하다. warehouse는 여러 product들의 재고를 가지고 있다. 우리가 order에게 warehouse로부터 채우라고 하면 두가지 반응이 있을 수 있다. warehouse에 order를 채울 만한 충분한 product가 있으면 order를 채우고 warehouse의 product 수는 그에 맞게 줄어든다. 충분한 product가 없으면 order는 채워지지 않고 warehouse에는 아무 일도 일어나지 않는다.
These two behaviors imply a couple of tests, these look like pretty conventional JUnit 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));
}
xUnit tests follow a typical four phase sequence: setup, exercise, verify, teardown. In this case the setup phase is done partly in the setUp method (setting up the warehouse) and partly in the test method (setting up the order). The call to order.fill
is the exercise phase. This is where the object is prodded to do the thing that we want to test. The assert statements are then the verification stage, checking to see if the exercised method carried out its task correctly. In this case there's no explicit teardown phase, the garbage collector does this for us implicitly.
xUnit 테스트는 전형적인 네가지 단계의 순서를 따른다: setup, exercise, verify, teardown. 위의 경우에서 setup단계는 setUp 메소드에서 일부 수행되고(warehouse 준비) 테스트 메소드에서도 일부 수행된다(order 준비). order.fill
호출은 exercise 단계이다. 이곳이 우리가 테스트하려는 것을 수행하도록 객체에게 시키는 곳이다. assert 문은 실행된 메소드가 작업을 올바로 수행했는지 확인하는 verification 단계가 된다. 여기에서는 명시적인 teardown 단계가 없다. garbage collector가 암묵적으로 그 일을 한다.
During setup there are two kinds of object that we are putting together. Order is the class that we are testing, but for Order.fill
to work we also need an instance of Warehouse. In this situation Order is the object that we are focused on testing. Testing-oriented people like to use terms like object-under-test or system-under-test to name such a thing. Either term is an ugly mouthful to say, but as it's a widely accepted term I'll hold my nose and use it. Following Meszaros I'll use System Under Test, or rather the abbreviation SUT.
setup 단계에서 두가지 종류의 객체를 만들었다. Order는 테스트하려는 클래스인데, Order.fill
이 동작하려면 Warehouse 객체도 필요하다. 이 상황에서 우리가 테스트에 초점을 두는 것은 Order 객체이다. 테스팅 분야에 있는 사람들은 이를 두고 object-under-test 혹은 system-under-test 라는 용어를 쓰기를 좋아한다. 어느 것 하나 발음하기 쉽지 않지만, 넓게 받아들여진 용어라서 할 수 없이 입닥치고 쓴다. Meszaros를 좇아 System Under Test, 줄여서 SUT라고 쓰겠다.
So for this test I need the SUT (Order
) and one collaborator (warehouse
). I need the warehouse for two reasons: one is to get the tested behavior to work at all (since Order.fill
calls warehouse's methods) and secondly I need it for verification (since one of the results of Order.fill is a potential change to the state of the warehouse). As we explore this topic further you'll see there we'll make a lot of the distinction between SUT and collaborators. (In the earlier version of this article I referred to the SUT as the "primary object" and collaborators as "secondary objects")
따라서 이 테스트에는 SUT (Order
)와 하나의 협력객체 (warehouse
)가 필요하다. warehouse는 두가지 이유에서 필요하다: 하나는 테스트하려는 동작이 실행되기라도 해야겠기에(Order.fill
은 warehouse의 메소드를 호출하니까). 둘째로는 검증에 필요하다(Order.fill
의 결과로서 warehouse의 상태에 잠재적 변화가 있으니까). 이 주제를 더 깊이 살펴보면서 우리가 SUT와 협력객체를 매우 크게 구분한다는 점을 알게 될 것이다. (이전 아티클에서 나는 SUT를 "주요 객체"로, 협력객체를 "부차적 객체"로 지칭했다)
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. As we'll see, mock objects enable a different approach to verification.
이런 스타일의 테스팅은 상태검증을 사용한 것이다: 메소드가 수행된 후 SUT와 협력객체의 상태를 살펴봄으로써 실행된 메소드가 올바로 동작했는지를 판단한다는 의미이다. 보게 되겠지만 모의객체를 사용함으로써 다른 접근법의 검증이 가능하다.
모의객체를 이용한 테스트(Tests with Mock Objects)
Now I'll take the same behavior and use mock objects. For this code I'm using the jMock library for defining mocks. jMock is a java mock object library. There are other mock object libraries out there, but this one is an up to date library written by the originators of the technique, so it makes a good one to start with.
이제 똑같은 동작을 가지고 모의객체를 사용할 것이다. 이 코드에서는 jMock라이브러리를 이용하여 모의객체를 만든다. 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());
}
Concentrate on testFillingRemovesInventoryIfInStock
first, as I've taken a couple of shortcuts with the later test.
첫째로 testFillingRemovesInventoryIfInStock
에 집중하자. 두번째 테스트는 몇가지 간단한 방법을 썼다.
To begin with, the setup phase is very different. For a start it's divided into two parts: data and expectations. The data part sets up the objects we are interested in working with, in that sense it's similar to the traditional setup. The difference is in the objects that are created. The SUT is the same - an order. However the collaborator isn't a warehouse object, instead it's a mock warehouse - technically an instance of the class Mock
.
우선 setup 단계가 매우 다르다. 시작 부분에 두가지로 나뉘어 진다: 데이타와 예측(expectation). 데이타부분은 작업하려는 객체들을 준비하는데, 그런 의미에서 전통적인 setup과 유사하다. 다른 점은 생성되는 객체에 있다. SUT는 같다 - order. 하지만 협력자가 warehouse가 아니라 모의객체 warehouse이다 - 기술적으로는 Mock
의 인스턴스이다.
The second part of the setup creates expectations on the mock object.The expectations indicate which methods should be called on the mocks when the SUT is exercised.
setup의 두번째 부분은 모의객체에 대한 예측을 준비하고 있다. 예측은 SUT를 돌려 보았을 때 모의객체의 어떤 메소드가 호출되었는지를 기술하는 것이다.
Once all the expectations are in place I exercise the SUT. After the exercise I then do verification, which has two aspects. I run asserts against the SUT - much as before. However I also verify the mocks - checking that they were called according to their expectations.
모든 예측이 준비되었으면 SUT를 돌려 본다. 돌려본 다음 검증을 하는데 여기에는 두가지 측면이 있다. SUT에 대해 assert하는 것 - 예전과 같다. 또 모의객체를 검증하는 것 - 예측에 맞게 호출되었는지 확인.
The key difference here is how we verify that the order did the right thing in its interaction with the warehouse. With state verification we do this by asserts against the warehouse's state. 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 the method doesn't change the state of the order there's no asserts at all.
여기서 핵심적인 차이는 order가 warehouse와 상호작용할 때 올바로 수행되었는지 어떻게 확인하느냐이다. 상태검증에서는 warehouse의 상태를 assert 함으로써 할 수 있다. 모의객체의 경우에는 행위검증을 사용한다. 여기서는 order가 warehouse에 대해 올바르게 호출했는지를 살펴보는 것을 의미한다. 우리는 이 확인을 setup에서 모의객체에게 예측치를 알려주고 검증시에는 스스로 확인해보라고 함으로써 대조를 한다. assert로 확인하는 것은 order뿐이고, 수행한 메소드가 order의 상태를 변경하지 않는다면 assert는 아예 필요 없다.
In the second test I do a couple of different things. Firstly I create the mock differently, using the mock
method in MockObjectTestCase rather than the constructor. This is a convenience method in the jMock library that means that I don't need to explicitly call verify later on, any mock created with the convenience method is automatically verified at the end of the test. I could have done this in the first test too, but I wanted to show the verification more explicitly to show how testing with mocks works.
두번째 테스트에서는 몇 가지 다른 일을 하고 있다. 첫째로, 모의객체를 다른 식으로 만든다. 생성자가 아니라 MockObjectTestCase의 mock
메소드를 사용한다. jMock 라이브러리가 제공하는 간편 메소드인데 그렇게 하면 명시적으로 verify를 호출할 필요가 없다. 이 간편 메소드를 이용하여 생성된 모의객체는 테스트 메소드가 끝날때 자동으로 검증이 수행된다. 첫째 테스트에서도 이럴 수 있었지만 모의객체를 이용한 테스팅이 어떻게 동작하는지 보여주기 위해 더 명시적으로 호출하는 것을 보였다.
The second different thing in the second test case is that I've relaxed the constraints on the expectation by using withAnyArguments
. The reason for this is that the first test checks that the number is passed to the warehouse, so the second test need not repeat that element of the test. If the logic of the order needs to be changed later, then only one test will fail, easing the effort of migrating the tests. As it turns out I could have left withAnyArguments
out entirely, as that is the default.
둘째로 withAnyArguments
를 써서 예측의 제약을 완화시켰다. 첫째 테스트가 warehouse에 전달된 숫자를 확인하기 때문인데 따라서 두번째 테스트는 같은 테스트를 반복할 필요가 없다. 만약 order의 로직이 변경될 필요가 생길 경우 하나의 테스트만 실패하게 되어 수고를 덜 수 있다. 알게 되겠지만 withAnyArguments
를 완전히 제거해도 되었다. 아무 것도 하지 않아도 기본적으로 이것이 수행되기 때문이다.
Using EasyMock
There are a number of mock object libraries out there. One that I come across a fair bit is EasyMock, both in its java and .NET versions. EasyMock also enable behavior verification, but has a couple of differences in style with jMock which are worth discussing. Here are the familiar tests again:
세상에는 많은 모의객체 라이브러리들이 있다. 꽤 자주 만나는 것에 EasyMock이라는 것이 있는데 자바와 닷넷 버전이 있다. EasyMock 역시 행위검증을 할 수 있는데 jMock과 스타일에서 몇가지 다른 점도 있어서 의논해 볼 가치가 있다.
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();
}
}
EasyMock uses a record/replay metaphor for setting expectations. For each object you wish to mock you create a control and mock object. The mock satisfies the interface of the secondary object, the control gives you additional features. To indicate an expectation you call the method, with the arguments you expect on the mock. You follow this with a call to the control if you want a return value. Once you've finished setting expectations you call replay on the control - at which point the mock finishes the recording and is ready to respond to the primary object. Once done you call verify on the control.
EasyMock 는 예측을 준비할 때에 녹화/재생 메타포를 사용한다. 모의객체의 기반이 되는 객체에 대해 control과 mock 을 만든다. mock은 부차적인(협력자 - 옮긴이) 객체의 인터페이스를 따르는 녀석이다. control은 추가적인 기능이 더 있다. 예측을 주려면 mock이 기대하는 인수로 메소드 호출을 하면 된다. 그 다음엔 반환값이 있으면 control로 알려준다. 예측을 세팅한 다음에는 control의 replay 메소드를 호출한다 - 이 지점에서 mock은 녹화를 끝내고 주요객체(SUT)의 동작에 반응할 준비가 된 것이다. 다 끝나면 control 의 verify를 호출한다.
It seems that while people are often fazed at first sight by the record/replay metaphor, they quickly get used to it. It has an advantage over the constraints of jMock in that you are making actual method calls to the mock rather than specifying method names in strings. This means you get to use code-completion in your IDE and any refactoring of method names will automatically update the tests. The downside is that you can't have the looser constraints.
사람들이 처음에는 녹화/재생 메타포에 대해 어리둥절해 하는 것처럼 보이기는 한데, 금방 익숙해진다. 문자열로 메소드를 지정하기보다는 직접 mock에 대해 메소드 호출을 하기 때문에 jMock의 제약 방식보다 낫다. 이것은 IDE의 자동완성을 사용할 수 있다는 의미이고 메소드 이름을 리팩토링하면 자동으로 테스트가 수정된다는 뜻이 된다.
The developers of jMock are working on a new version which will use other techniques to allow you use actual method calls.
jMock 개발자들은 새버전에서는 이처럼 실제 메소드를 호출할 수 있도록 하려고 작업중이다.
모의객체와 스텁의 차이 (The Difference Between Mocks and Stubs)
When they were first introduced, many people easily confused mock objects with the common testing notion of using stubs. Since then it seems people have better understood the differences (and I hope the earlier version of this paper helped). However to fully understand the way people use mocks it is important to understand mocks and other kinds of test doubles. ("doubles"? Don't worry if this is a new term to you, wait a few paragraphs and all will be clear.)
처음 소개되었을 때, 많은 사람들이 모의객체를 일반적으로 알려진 개념인 스텁과 쉽게 혼동했다. 이후에는 그 차이점을 더 잘 이해하게 된 듯 하다 (이 문서의 처음 버전이 도움이 되었다고 생각한다). 그러나 모의객체를 사용하는 방법을 완전히 이해하려면 모의객체와 여러 종류의 테스트더블을 이해하는 것이 중요하다 ("더블"? 이 용어가 처음이라 해도 걱정하지 말라. 몇 문장 이후에 분명해 질 것이다.).
When you're doing testing like this, you're focusing on one element of the software at a time -hence the common term unit testing. The problem is that to make a single unit work, you often need other units - hence the need for some kind of warehouse in our example.
당신이 위에서처럼 테스팅을 수행할 때에, 한 순간에 하나의 요소에 집중하게 된다 - 유닛테스팅이라는 익숙한 용어는 그래서이다. 문제는 하나의 유닛이 동작하려면 다른 유닛이 종종 필요하다는 것이다 - 예제에서 warehouse와 같은 것이 필요하다.
In the two styles of testing I've shown above, the first case uses a real warehouse object and the second case uses a mock warehouse, which of course isn't a real warehouse object. Using mocks is one way to not use a real warehouse in the test, but there are other forms of unreal objects used in testing like this.
위에 보인 두가지 테스팅 스타일에서, 첫째 경우는 진짜 warehouse객체를 사용했고 두번째 경우는 모의객체를 사용했는데 물론 이건 진짜 warehouse가 아니다. 모의객체를 사용하는 것은 테스트에서 진짜 warehouse를 사용하지 않는 단지 하나의 방식일 뿐인데, 이처럼 테스팅에서 실제 객체가 아닌 것을 사용하는 여러가지 방식이 있다.
The vocabulary for talking about this soon gets messy - all sorts of words are used: stub, mock, fake, dummy. For this article I'm going to follow the vocabulary of Gerard Meszaros's book. It's not what everyone uses, but I think it's a good vocabulary and since it's my essay I get to pick which words to use.
이런 것들을 지칭하는 어휘들은 곧 지저분해 진다 - 모든 종류의 단어들이 사용된다: 스텁, 모의객체, fake, dummy. 이 아티클에서는 Gerard Meszaros 책의 어휘들을 따를 것이다. 모든 사람들이 사용하는 것은 아니지만 나는 좋은 어휘선택이라 생각하고 있고 또 이건 내 에세이이기 때문에 내가 어떤 어휘를 사용할지 고를 것이다.
Meszaros uses the term Test Double as the generic term for any kind of pretend object used in place of a real object for testing purposes. The name comes from the notion of a Stunt Double in movies. (One of his aims was to avoid using any name that was already widely used.) Meszaros then defined four particular kinds of double:
Meszaros는 테스트더블을 테스팅을 목적으로 진짜 객체대신 사용되는 모든 종류의 위장 객체를 지칭하는 일반적 용어로서 사용한다. 이 이름은 영화에서 Stunt Double이라는 개념에서 나왔다. (그에겐 이미 널리 사용되는 이름을 피하려는 의도도 있다.) Meszaros는 그런 다음 네가지 특정 종류의 더블을 정의했다.
- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Dummy 객체는 전달되기만 하고 실제 사용되지는 않는다. 보통 파라미터 리스트를 채우는데에 사용된다.
- Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
- Fake 객체는 동작하는 구현이 있다. 하지만 운영시에는 사용할 수 없는 간단한 형태이다. (인메모리 데이타베이스가 좋은 예이다).
- 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. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.
- 스텁은 테스트시 호출이 되면 미리 준비된 답변으로 응답하는데, 테스트시에 프로그램된 것 이외의 것에 대해서는 응답하지 않는다. 스텁은 호출에 대한 정보를 기록할 수도 있을 것이다. 이메일 게이트웨이 스텁은 '보낸' 메시지들 혹은 몇개의 메시지를 '보냈'는가를 기억하는 것이다.
- Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
- 모의객체는 지금 여기서 얘기하는 것이다: 수신하기를 기대하는 호출의 명세(specification)인 예측으로 미리 프로그램 된 객체이다.
Of these kinds of doubles, only mocks insist upon behavior verification. The other doubles can, and usually do, use state verification. Mocks actually do behave like other doubles during the exercise phase, as they need to make the SUT believe it's talking with its real collaborators - but mocks differ in the setup and the verification phases.
이러한 더블중에 모의객체만이 행위검증 사용을 추구한다. 다른 더블은 상태검증까지도 할 수 있고, 또 보통 그렇게 쓴다. 모의객체는 excercise단계에서는 실제 다른 더블과 같이 동작한다. 실제 협력객체들과 대화하고 있다는 것을 SUT가 믿도록 할 필요가 있으니까. 그러나 setup과 verification단계에서는 다르다.
To explore test doubles a bit more, we need to extend our example. Many people only use a test double if the real object is awkward to work with. A more common case for a test double would be if we said that we wanted to send an email message if we failed to fill an order. The problem is that we don't want to send actual email messages out to customers during testing. So instead we create a test double of our email system, one that we can control and manipulate.
테스트더블을 조금 더 자세히 살펴보려면 예제를 조금 확장할 필요가 있다. 많은 사람들은 실제 객체를 다루기 불편할 때에만 테스트더블을 사용하는데, order 채우기가 실패했을 때 메일을 보내는 경우가 테스트더블에 더 일반적 경우일 것이다. 문제는 테스트중에 고객들에게 실제 이메일을 보내고 싶지는 않다는 것이다. 따라서 이메일 시스템을 위한 테스트더블을 만든다. 우리가 제어 및 조작할 할 수 있는 그것으로.
Here we can begin to see the difference between mocks and stubs. If we were writing a test for this mailing behavior, we might write a simple stub like this.
여기에서 모의객체와 스텁의 차이를 보게 된다. 메일링 동작을 테스트하려면 아래와 같은 간단한 스텁을 작성할 수 있겠다.
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();
}
}
We can then use state verification on the stub like this.
그런 다음 스텁으로 상태검증을 한다.
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());
}
Of course this is a very simple test - only that a message has been sent. We've not tested it was send to the right person, or with the right contents, but it will do to illustrate the point.
물론 이건 아주 간단한 테스트이다 - 메시지가 보내졌다는 것을 테스트한다. 올바른 수신자에게 보내졌는지, 올바른 내용으로 보내졌는지는 테스트하지 않았지만 핵심을 보여주고 있다.
Using mocks this test would look quite different.
모의객체를 사용하면 테스트는 꽤 달라진다.
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());
}
}
In both cases I'm using a test double instead of the real mail service. There is a difference in that the stub uses state verification while the mock uses behavior verification.
두가지 경우 모두 진짜 메일서비스 대신 테스트더블을 사용하고 있다. 차이점은 스텁은 상태검증을 사용하고, 모의객체는 행위검증을 사용한다는 점이다.
In order to use state verification on the stub, I need to make some extra methods on the stub to help with verification. As a result the stub implements MailService
but adds extra test methods.
스텁에 상태검증을 하려고 검증에 도움이 될 추가적인 메소드를 만들었다. 스텁은 MailService
인터페이스를 구현하고 추가된 테스트 메소드도 있다.
Mock objects always use behavior verification, a stub can go either way. Meszaros refers to stubs that use behavior verification as a Test Spy. The difference is in how exactly the double runs and verifies and I'll leave that for you to explore on your own.
모의객체는 항상 행위검증을 사용하고, 스텁은 둘 다 가능하다. Meszaros는 행위검증을 하는 스텁을 가리켜 테스트스파이(Test Spy)라고 부른다. 차이점은 더블이 얼마나 정확히 수행과 검증을 하느냐인데 이것은 독자 여러분들이 스스로 탐구하도록 남겨 두겠다.
고전적 테스팅과 모의객체 테스팅 (Classical and Mockist Testing)
Now I'm at the point where I can explore the second dichotomy: that between classical and mockist TDD. The big issue here is when to use a mock (or other double).
이제 두번째 이분법을 살펴볼 시점이다: 고전적 테스팅과 모의객체 테스팅. 여기서 큰 이슈는 언제 모의객체(혹은 다른 더블)를 사용할 것인가이다.
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.
고전적 TDD 스타일은 가능하면 진짜 객체를 사용하고 진짜 객체를 사용하기가 만만하지 않으면 더블을 사용한다. 그러므로 고전적 TDD 실천가들은 warehouse에는 진짜 warehouse를 사용하고 메일 서비스에는 더블을 사용할 것이다. 더블의 종류는 그리 중요하지 않다.
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.
하지만 모의객체적 TDD 실천가는 관심있는 행위를 가진 모든 객체에 모의객체를 사용하려 한다. 예제에서는 warehouse와 메일서비스 모두에 대해서이다.
Although the various mock frameworks were designed with mockist testing in mind, many classicists find them useful for creating doubles.
비록 다양한 모의객체 프레임워크들이 모의객체 테스팅을 염두에 두고 설계되었지만, 많은 고전주의자도 이것을 더블 작성에 도움이 되는 것으로 생각하고 있다.
An important offshoot of the mockist style is that of Behavior Driven Development (BDD). BDD was originally developed by my colleague Dan North as a technique to better help people learn Test Driven Development by focusing on how TDD operates as a design technique. This led to renaming tests as behaviors to better explore where TDD helps with thinking about what an object needs to do. BDD takes a mockist approach, but it expands on this, both with its naming styles, and with its desire to integrate analysis within its technique. I won't go into this more here, as the only relevance to this article is that BDD is another variation on TDD that tends to use mockist testing. I'll leave it to you to follow the link for more information.
모의객체적 스타일중에 중요한 한 분파로서 행위주도개발 Behavior Driven Development (BDD)이 있다. BDD는 처음에 내 동료 Dan North에 의해, 사람들이 테스트 주도 개발을 배우는 데에 더욱 도움을 주기 위한 기술로서 개발되었는데 TDD가 설계기술로서 어떻게 동작하는지에 중점을 두었다. 이후 객체가 해야 할 것에 대해 생각하는 데에 TDD가 도움을 주는 지점을 더 잘 살펴보도록 테스트의 이름을 행위로 바꾸는 데에까지 나아갔다. BDD는 모의객체 접근법을 취한다. 그러나 여기에서 더해 작명 스타일과 분석작업 통합이라는 목표 양쪽으로 확장되었다. 이 글에서는 더 깊이 들어가지는 않겠다. BDD가 모의객체 테스팅의 흐름 중에서 또 다른 변형 TDD라는 사실이 이 아티클과의 유일한 관련성이니까. 더 많은 정보가 필요하면 여러분이 링크를 따라가기 바란다.
차이점들 중에서 선택하기 (Choosing Between the Differences)
In this article I've explained a pair of differences: state or behavior verification / classic or mockist TDD. What are the arguments to bear in mind when making the choices between them? I'll begin with the state versus behavior verification choice.
이 아티클에서 나는 두가지 차이점 쌍을 설명했다: 상태검증과 행위검증 / 고전적 TDD와 모의객체적 TDD. 선택할 때에 염두에 두어야 할 점은 무엇인가? 상태검증과 행위검증부터 시작해 보겠다.
The first thing to consider is the context. Are we thinking about an easy collaboration, such as order and warehouse, or an awkward one, such as order and mail service?
첫번째로 생각해야 할 것은 컨텍스트이다. order와 warehouse 사이처럼 객체들간의 협력이 간단한가 아니면 order와 메일서비스처럼 쉽지 않은가?
If it's an easy collaboration then the choice is simple. If I'm a classic TDDer I don't use a mock, stub or any kind of double. I use a real object and state verification. If I'm a mockist TDDer I use a mock and behavior verification. No decisions at all.
간단한 협력이라면 선택은 간단하다. 내가 만약 고전적 TDD실천가라면 모의객체나 스텁 혹은 어떤 종류의 더블도 쓰지 않는다. 진짜 객체를 쓰고 상태검증을 한다. 모의객체적 TDD 실천가라면 모의객체와 행위검증을 쓴다. 선택이 필요 없다.
If it's an awkward collaboration, then there's no decision if I'm a mockist - I just use mocks and behavior verification. If I'm a classicist then I do have a choice, but it's not a big deal which one to use. Usually classicists will decide on a case by case basis, using the easiest route for each situation.
간단하지 않은 협력의 경우에서, 모의객체주의자이면 선택이 필요없다. 그냥 모의객체와 행위검증을 쓴다. 고전주의자이면 선택이 필요한데 그리 대단한 것은 아니다. 보통 고전주의자들은 그때 그때에 따라 가장 쉬운 방법을 사용하려 할 것이다.
So as we see, state versus behavior verification is mostly not a big decision. The real issue is between classic and mockist TDD. As it turns out the characteristics of state and behavior verification do affect that discussion, and that's where I'll focus most of my energy.
보았던 것처럼 상태검증이냐 행위검증이냐는 그리 큰 결정사항이 아니다. 진짜 이슈는 고전적 TDD와 모의객체적 TDD 사이의 선택에 있다. 이제 드러나겠지만 상태검증과 행위검증의 특징이 논의에 영향을 미치므로 나는 내 에너지를 여기에 집중하려 한다.
But before I do, let me throw in an edge case. Occasionally you do run into things that are really hard to use state verification on, even if they aren't awkward collaborations. A great example of this is a cache. The whole point of a cache is that you can't tell from its state whether the cache hit or missed - this is a case where behavior verification would be the wise choice for even a hard core classical TDDer. I'm sure there are other exceptions in both directions.
하지만 그러기 전에 특수한 경우를 살펴보자. 때때로 다루기 힘든 협력관계가 아닌데도 상태검증을 하기 쉽지 않은 경우가 있다. 이 예에 딱 맞는 것이 캐시이다. 캐시에서의 문제점은 캐시의 상태를 가지고서 캐시가 작동한건지 지나가 버린건지 알 수 없다는 것이다 - 이 경우는 하드코어 고전주의자도 동작검증을 선택하는 것이 더 현명한 케이스이다. 당연히 그 역의 관계에 있는 예외사항도 있으리라 생각한다.
As we delve into the classic/mockist choice, there's lots of factors to consider, so I've broken them out into rough groups.
고전주의/모의객체주의 사이의 선택이라는 문제에 깊이 들어갈수록 고려해야 할 사항들이 많다. 그래서 몇 개의 그룹으로 문제를 나누어 보았다.
TDD 운전 (Driving TDD)
Mock objects came out of the XP community, and one of the principal features of XP is its emphasis on Test Driven Development - where a system design is evolved through iteration driven by writing tests.
모의객체는 XP 커뮤니티로부터 왔고, XP의 주요 특징 중 하나가 테스트 주도 개발을 중요하게 여기는 것이다 - 여기서 시스템의 설계는 테스트 작성에 의해 주도되는 이터레이션에 의해 진화한다.
Thus it's no surprise that the mockists particularly talk about the effect of mockist testing on a design. In particular they advocate a style called need-driven development. With this style you begin developing a story by writing your first test for the outside of your system, making some interface object your SUT. By thinking through the expectations upon the collaborators, you explore the interaction between the SUT and its neighbors - effectively designing the outbound interface of the SUT.
따라서 모의객체주의자들이 설계시 모의객체 테스팅의 효과에 대해 특히 강조하는 것은 놀랄 만한 일은 아니다. 특히나 그들은 필요성 주도 개발 (need-driven development) 스타일을 옹호한다. 이 스타일에서는 만들고자 하는 시스템 외부에서 SUT의 인터페이스를 만들면서 테스트를 작성함으로써 스토리 개발을 시작한다. 협력객체에 대해서는 예측을 생각함으로써 SUT와 이웃객체들 사이의 상호작용을 개척해 나간다 - 결과적으로 SUT의 외부 인터페이스를 설계하는 것이다.
Once you have your first test running, the expectations on the mocks provide a specification for the next step and a starting point for the tests. You turn each expectation into a test on a collaborator and repeat the process working your way into the system one SUT at a time. This style is also referred to as outside-in, which is a very descriptive name for it. It works well with layered systems. You first start by programming the UI using mock layers underneath. Then you write tests for the lower layer, gradually stepping through the system one layer at a time. This is a very structured and controlled approach, one that many people believe is helpful to guide newcomers to OO and TDD.
첫번째 테스트가 돌아가게 되면, 모의객체에 대한 예측은 다음 단계의 명세가 되고 테스트들의 시작점이 된다. 각각의 예측을 협력객체에 대한 테스트로 변경하고 한번에 하나의 SUT로 작업하면서 이 절차를 반복한다. 이 스타일은 outside-in 으로 알려져 있는데, 그에 걸맞은 매우 좋은 묘사이다. 이것은 계층적 시스템에서도 적용할 수 있다. 모의객체를 아래에 두고 UI 프로그래밍을 시작한다. 하위 레이어에 대해 테스트를 작성하고, 단계적으로 한번에 하나의 레이어를 밟아 나간다. 이것은 매우 구조화된 그리고 잘 제어된 접근인데 많은 사람들이 신참자들에게 OO와 TDD를 가이드할 때 도움이 된다고 믿고 있다.
Classic TDD doesn't provide quite the same guidance. You can do a similar stepping approach, using stubbed methods instead of mocks. To do this, whenever you need something from a collaborator you just hard-code exactly the response the test requires to make the SUT work. Then once you're green with that you replace the hard coded response with a proper code.
고전적 TDD는 이 정도와 같은 가이드를 제공하지 않는다. 유사한 단계를 밟아 접근 할 수도 있겠다. 모의객체 대신 스텁 메소드를 써서. 그럴려면 협력객체가 어떤 일을 해주기를 원할 때에, SUT가 동작하기 위해 테스트가 요구하는 응답을 정확하게 하드코딩 한다. 그런 다음 초록막대가 되면 하드코딩한 응답을 적당한 코드로 바꾼다.
But classic TDD can do other things too. A common style is middle-out. In this style you take a feature and decide what you need in the domain for this feature to work. You get the domain objects to do what you need and once they are working you layer the UI on top. Doing this you might never need to fake anything. A lot of people like this because it focuses attention on the domain model first, which helps keep domain logic from leaking into the UI.
하지만 고전적 TDD는 다르게 할 수도 있다. 통상적인 스타일은 middle-out이다. 이 스타일에서는 어떤 기능을 택하고 이 기능이 동작하려면 도메인에서 무엇이 필요한지 결정한다. 필요한 것을 도메인 객체들이 수행하게 하고 일단 동작하면 그 위에 UI를 올린다. 이렇게 하면 어떤 것도 모조할 필요는 없을 것이다. 많은 사람들이 이 방식을 좋아한다. 왜냐하면 처음부터 도메인 모델에 주의를 집중하도록 하여 도메인 로직이 UI로 흘러 들어 가지 않도록 하기 때문이다.
I should stress that both mockists and classicists do this one story at a time. There is a school of thought that builds applications layer by layer, not starting one layer until another is complete. Both classicists and mockists tend to have an agile background and prefer fine-grained iterations. As a result they work feature by feature rather than layer by layer.
모의객체주의자나 고전주의자나 둘 다 한번에 하나의 스토리로만 이런 작업을 한다는 것을 강조해야겠다. 다른 레이어가 완료될 때까지 레이어개발을 시작하지 않는, 즉 레이어 단위로 개발하는 생각을 가르치는 학교가 있다. 모의객체주의자나 고전주의자나 둘 다 애자일 백그라운드를 가지고 있고 정교한 이터레이션을 선호한다. 결과적으로 둘 다 레이어 단위로 개발하지 않고 기능 단위로 개발한다.
픽스처 준비 (Fixture Setup)
With classic TDD, you have to create not just the SUT but also all the collaborators that the SUT needs in response to the test. While the example only had a couple of objects, real tests often involve a large amount of secondary objects. Usually these objects are created and torn down with each run of the tests.
고전주의 TDD에서는, SUT 뿐만 아니라 테스트의 필요에 따라 SUT가 필요로 하는 모든 협력 객체들도 만든다. 예제에서는 몇개의 객체 뿐이었지만 실제 테스트에서는 종종 많은 양의 부차적인 객체들이 필요하다. 보통 이 객체들은 각각의 테스트 실행 때마다 생성되고 사라진다.
Mockist tests, however, only need to create the SUT and mocks for its immediate neighbors. This can avoid some of the involved work in building up complex fixtures (At least in theory. I've come across tales of pretty complex mock setups, but that may be due to not using the tools well.)
그러나 모의객체를 이용한 테스트에서는, SUT와 그에 인접한 모의객체만을 작성하면 된다. 복잡한 픽스처를 구성하는 작업을 피할 수 있다 (최소한 이론적으로는 그렇다. 꽤 복잡하게 모의객체를 준비하는 경우도 보긴 했는데 툴을 제대로 사용하지 못한 것에 기인하는 것 같다).
In practice, classic testers tend to reuse complex fixtures as much as possible. In the simplest way you do this by putting fixture setup code into the xUnit setup method. More complicated fixtures need to be used by several test classes, so in this case you create special fixture generated classes. I usually call these Object Mothers, based on a naming convention used on an early ThoughtWorks XP project. Using mothers is essential in larger classic testing, but the mothers are additional code that need to be maintained and any changes to the mothers can have significant ripple effects through the tests. There also may be a performance cost in setting up the fixture - although I haven't heard this to be a serious problem when done properly. Most fixture objects are cheap to create, those that aren't are usually doubled.
실전에서는, 고전주의자들은 복잡한 픽스처를 가능한 한 재사용하려 한다. 가장 간단한 방법은 픽스처 준비 코드를 xUnit의 setup메소드에 넣는 것이다. 좀더 복잡한 픽스처는 여러 테스트에서 사용될 필요가 있을텐데, 이 경우 픽스처로부터 특수 클래스를 만든다. 나는 이것을 보통 Object Mothers라고 부르는데, 예전 ThoughtWorks의 XP 프로젝트에서 사용된 명명관례에 따른 것이다. 덩치가 있는 고전주의 테스팅에서 mother의 사용은 필수적이다. 그러나 mother는 추가된 코드이며 관리되어야 하고 mother에 대한 어떤 변경도 테스트에 중요한 반향을 일으킬 수 있다. 픽스처 준비에 성능 관점의 비용도 있을 수 있다 - 적절히 수행될 경우 심각한 문제가 발생했다는 얘기를 들은 적은 없지만. 대부분의 픽스처는 생성 비용이 크지 않고, 만약 크다면 보통 더블이 사용된다.
As a result I've heard both styles accuse the other of being too much work. Mockists say that creating the fixtures is a lot of effort, but classicists say that this is reused but you have to create mocks with every test.
결국 내가 들은 것은 두 스타일 모두 상대에게 너무 작업이 많다고 하는 것이다. 모의객체주의자들은 픽스처를 만드는 노력이 크다 하고, 고전주의자들은 픽스처는 재사용된다 하며 반면 모의객체는 모든 테스트마다 만들어야 한다고 말한다.
테스트의 고립성 (Test Isolation)
If you introduce a bug to a system with mockist testing, it will usually cause only tests whose SUT contains the bug to fail. With the classic approach, however, any tests of client objects can also fail, which leads to failures where the buggy object is used as a collaborator in another object's test. As a result a failure in a highly used object causes a ripple of failing tests all across the system.
모의객체로 테스트하는 시스템에서 버그가 생기면, 보통 SUT가 버그를 가진 테스트만 실패하게 된다. 그러나 고전적 접근법은 이 객체를 이용하는 클라이언트가 있는 모든 테스트도 실패하며, 다른 객체를 테스트할 때 버그를 가진 객체가 협력객체로 사용되는 곳에서 모두 실패하게 된다. 결과적으로 매우 많이 사용되는 객체가 실패하면 시스템 전체에 걸쳐 실패하는 테스트의 파장이 일어난다.
Mockist testers consider this to be a major issue; it results in a lot of debugging in order to find the root of the error and fix it. However classicists don't express this as a source of problems. Usually the culprit is relatively easy to spot by looking at which tests fail and the developers can tell that other failures are derived from the root fault. Furthermore if you are testing regularly (as you should) then you know the breakage was caused by what you last edited, so it's not difficult to find the fault.
모의객체주의자들은 이것을 큰 이슈로 여긴다; 에러의 원인을 찾고 고치느라 많은 디버깅 노력을 낳는다는 것이다. 그러나 고전주의자들은 이것을 문제 발생의 근원이라고 보지 않는다. 어떤 테스트가 실패하는지를 살펴보면 일반적으로 문제 지점을 찾기는 비교적 쉽고 개발자들은 그 오류의 근원으로부터 다른 실패도 알아챌 수 있다. 더구나 규칙적으로(아마 당신도 그렇겠지만) 테스팅을 한다면 마지막으로 고친 것 때문에 깨진 것을 알게 되고, 오류를 찾는 것은 어렵지 않다.
One factor that may be significant here is the granularity of the tests. Since classic tests exercise multiple real objects, you often find a single test as the primary test for a cluster of objects, rather than just one. If that cluster spans many objects, then it can be much harder to find the real source of a bug. What's happening here is that the tests are too coarse grained.
여기서 의미심장할 수도 있는 한가지 요소는 테스트의 정밀도이다. 고전적 테스트에서는 여러개의 실제 객체로 테스트를 수행하기 때문에, 하나의 테스트가, 하나의 객체가 아닌 여러 객체의 클러스터에 대해 수행되는 주요 테스트로서의 역할을 하고 있다는 것을 발견하게 될 것이다. 그 클러스터가 여러 객체에 걸쳐 있으면, 버그의 진짜 발생지를 찾기가 더욱 어려울 수 있다. 여기서 지금 일어나고 있는 일은 테스트가 너무 거칠게 작성되었다는 것이다.
It's quite likely that mockist tests are less likely to suffer from this problem, because the convention is to mock out all objects beyond the primary, which makes it clear that finer grained tests are needed for collaborators. That said, it's also true that using overly coarse grained tests isn't necessarily a failure of classic testing as a technique, rather a failure to do classic testing properly. A good rule of thumb is to ensure that you separate fine-grained tests for every class. While clusters are sometimes reasonable, they should be limited to only very few objects - no more than half a dozen. In addition, if you find yourself with a debugging problem due to overly coarse-grained tests, you should debug in a test driven way, creating finer grained tests as you go.
확실히 모의객체 테스트는 이런 문제로 어려움을 겪을 가능성은 더 적을 것이다. 주요 객체가 아닌 모든 객체들을 모의객체로 만드는 것이 관습이어서 협력객체에 대해 정교하게 테스트를 만들어야 한다는 것이 명확하니까. 그렇다 할지라도, 기법으로서의 과도하게 거친 테스트가 필연적으로 고전적 테스팅의 실패일 수 없다는 것도 사실이다. 오히려 고전적 테스팅을 적절히 수행하는 것에 대한 실패인 것이다. 여기서 좋은 경험법칙은 모든 클래스에 대해 정교한 테스트를 확실히 분리해 내는 것이다. 객체 클러스터가 때로는 합리적일지라도, 매우 적은 수의 객체로 제한되어야 한다 - 6개 이하로. 또, 과도하게 거친 테스트로 인해 디버깅 문제를 겪고 있다면, 정교한 테스트를 만들어 나가면서 테스트 주도 방식으로 디버깅을 해야 한다.
In essence classic xunit tests are not just unit tests, but also mini-integration tests. As a result many people like the fact that client tests may catch errors that the main tests for an object may have missed, particularly probing areas where classes interact. Mockist tests lose that quality. In addition you also run the risk that expectations on mockist tests can be incorrect, resulting in unit tests that run green but mask inherent errors.
본질적으로 고전적 xunit 테스트는 단지 유닛테스트일 뿐 아니라, 작은-통합 테스트이기도 하다. 그래서 많은 사람들이 한 객체를 테스트할 때에 놓쳤을 지 모를 에러를, 클라이언트 테스트가 잡을 수 있다는 사실을 좋아한다. 특히 클래스들이 상호작용하는 영역을 조사하면서 말이다. 모의객체 테스트는 그런 품질을 잃게 된다. 게다가 모의객체 테스트에서 예측이 올바르지 않은데도 유닛테스트는 녹색막대이고 내재된 에러를 막아버리는 결과를 초래할 위험도 안게 된다.
It's at this point that I should stress that whichever style of test you use, you must combine it with coarser grained acceptance tests that operate across the system as a whole. I've often come across projects which were late in using acceptance tests and regretted it.
강조해야 할 지점은 여기이다. 어떤 스타일의 테스트를 하든, 전체 시스템에 걸쳐 수행되는 거친 정밀도의 인수테스트와 결합해야 한다. 인수테스트가 늦어 후회하는 프로젝트들을 종종 보아왔다.
테스트와 구현의 결합성 (Coupling Tests to Implementations)
When you write a mockist test, you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers. A classic test only cares about the final state - not how that state was derived. Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break.
모의객체 테스트를 작성하는 것은, SUT가 그것의 공급자와 올바로 대화하는지 확인하기 위해 SUT의 나가는 호출을 테스트하는 것이라 볼 수 있다. 고전주의 테스트는 최종 상태에만 신경 쓴다 - 그 상태가 어떻게 도출되었는지가 아니다. 따라서 모의객체 테스트는 메소드의 구현에 많이 결합되어 있다. 협력객체에 대한 호출 방식을 변경하면 일반적으로 모의객체 테스트는 실패하게 된다.
This coupling leads to a couple of concerns. The most important one is the effect on Test Driven Development. With mockist testing, writing the test makes you think about the implementation of the behavior - indeed mockist testers see this as an advantage. Classicists, however, think that it's important to only think about what happens from the external interface and to leave all consideration of implementation until after you're done writing the test.
이런 결합은 몇가지 우려를 낳는다. 가장 중요한 하나는 테스트주도개발에 대한 영향이다. 모의객체 테스팅에서는 테스트 작성시 행위 구현에 대해 생각하게 만든다 - 사실 모의객체 테스터들은 이것을 장점으로 본다. 그러나 고전주의자들은 외부 인터페이스 호출로부터 어떤 일이 일어나는지에 대해서만 생각하고 구현의 모든 고려사항들은 테스트 작성이 끝난 이후로 남기는 것이 중요하다고 생각한다.
Coupling to the implementation also interferes with refactoring, since implementation changes are much more likely to break tests than with classic testing.
구현에 결합되는 것은 리팩토링에도 간섭을 일으키는데, 구현이 바뀌면 고전적 테스팅보다 테스트를 깨트릴 가능성이 훨씬 더 많기 때문이다.
This can be worsened by the nature of mock toolkits. Often mock tools specify very specific method calls and parameter matches, even when they aren't relevant to this particular test. One of the aims of the jMock toolkit is to be more flexible in its specification of the expectations to allow expectations to be looser in areas where it doesn't matter, at the cost of using strings that can make refactoring more tricky.
이러한 사정은 모의객체 툴킷들의 본성에 의해서 더 나빠진다. 종종 모의객체 툴들은, 지금의 테스트와 관련 없는 때에도, 매우 구체적으로 메소드 호출을 지정하거나 파라미터를 맞출 것을 요구한다. jMock의 목표중 하나는 그리 상관없는 곳에서는 좀 더 느슨하게 예측할 수 있도록 하여 예측지정에 있어 더 유연해 지자는 것이다. 그러나, 문자열을 사용함으로서 리팩토링이 더 까다로워지긴 했다.
설계 스타일 (Design Style)
One of the most fascinating aspects of these testing styles to me is how they affect design decisions. As I've talked with both types of tester I've become aware of a few differences between the designs that the styles encourage, but I'm sure I'm barely scratching the surface.
이러한 테스팅 스타일들이 내게 있어 마음을 끄는 측면들 중 하나는 그것들이 설계 결정에 어떻게 영향을 미치느냐 하는 것이다. 두가지 타입의 테스터들과 얘기해 보면서 나는 각 스타일이 장려하는 설계간의 몇 가지 차이점들에 대해 알게 되었다. 하지만 난 가까스로 표면만 긁고 있을 뿐이라고 생각한다.
I've already mentioned a difference in tackling layers. Mockist testing supports an outside-in approach while developers who prefer a domain model out style tend to prefer classic testing.
레이어를 다루는 데 있어서 차이점은 이미 언급했다. 모의객체 테스팅은 외부에서 내부로 접근하는 방식이며, 도메인모델로부터 펼쳐나가는 스타일을 좋아하는 개발자들은 고전적 테스팅을 선호한다.
On a smaller level I noticed that mockist testers tend to ease away from methods that return values, in favor of methods that act upon a collecting object. Take the example of the behavior of gathering information from a group of objects to create a report string. A common way to do this is to have the reporting method call string returning methods on the various objects and assemble the resulting string in a temporary variable. A mockist tester would be more likely to pass a string buffer into the various objects and get them to add the various strings to the buffer - treating the string buffer as a collecting parameter.
더 작은 레벨에서 살펴보면 나는 모의객체 테스터들이 간편하게 값을 리턴하는 메소드들을 사용하는 경향이 있으며 이로서 결과를 취합하는 객체를 선호하는 것을 발견했다. 보고용 문자열을 만들기 위해 한 그룹의 객체로부터 정보를 추출하는 행위에 대한 예를 보자. 이를 위한 일반적인 방법은 그 보고용 메소드가 객체들에 대해 문자열을 리턴하는 메소드를 호출하게 하여 결과 문자열을 임시 변수에 모으는 것이다. 모의객체 테스터는 아마도 각 객체에 문자열 버퍼를 전달하여 객체들이 버퍼에 문자열을 추가하도록 할 것이다 - 문자열 버퍼를 취합 파라미터로 쓰는 것이다.
Mockist testers do talk more about avoiding 'train wrecks' - method chains of style of getThis().getThat().getTheOther()
. Avoiding method chains is also known as following the Law of Demeter. While method chains are a smell, the opposite problem of middle men objects bloated with forwarding methods is also a smell. (I've always felt I'd be more comfortable with the Law of Demeter if it were called the Suggestion of Demeter.)
모의객체 테스터들은 열차사고('train wrecks') - getThis().getThat().getTheOther()
와 같은 스타일의 메소드 체인 - 을 피하는 것에 대해 더 많이 얘기하고 있다. 메소드 체인을 피하는 것은 디미터 법칙(Law of Demeter)을 따르는 것으로도 알려져 있다. 메소드 체인은 냄세이기도 하지만, 메소드 전달로 점철된 미들맨 문제 역시 냄세이기도 하다. (나는 지금껏 항상 Law of Demeter 보다는 Suggestion of Demeter라고 불렸다면 더 납득이 쉬웠을 것이라고 느낀다.)
One of the hardest things for people to understand in OO design is the "Tell Don't Ask" principle, which encourages you to tell an object to do something rather than rip data out of an object to do it in client code. Mockists say that using mockist testing helps promote this and avoid the getter confetti that pervades too much of code these days. Classicists argue that there are plenty of other ways to do this.
사람들이 OO설계에서 이해하기 가장 힘든 것 중 하나는 "요청하지 말고 시키기 원칙(Tell Don't Ask" principle)인데, 이 원칙은 클라이언트 코드에서 무언가 하려고 어떤 객체로부터 데이타를 꺼내는 것보다는 그 객체에게 무엇을 하라 지시하라고 독려하는 것이다. 모의객체주의자들은 모의객체 테스팅이 이러한 방식을 진작하고 요즘 코드의 대부분에 퍼져 있는 접근자 세례를 피하는데 도움이 된다고 말한다. 고전주의자들은 그 방법 말고도 더 많은 다양한 방법이 있다고 말한다.
An acknowledged issue with state-based verification is that it can lead to creating query methods only to support verification. It's never comfortable to add methods to the API of an object purely for testing, using behavior verification avoids that problem. The counter-argument to this is that such modifications are usually minor in practice.
상태기반 검증에서 알려진 이슈가 단지 검증에 쓰려고 접근자 메소드를 만들게 된다는 것이다. 테스팅만을 위해서 어떤 객체의 API에 메소드를 추가하는 것은 전혀 납득할 수 없는 것인데, 행위 검증을 사용하면 그런 문제를 피할 수 있다. 이에 대한 반대 주장으로는 그런 변경은 실제로는 보통 사소하다는 것이다.
Mockists favor role interfaces and assert that using this style of testing encourages more role interfaces, since each collaboration is mocked separately and is thus more likely to be turned into a role interface. So in my example above using a string buffer for generating a report, a mockist would be more likely to invent a particular role that makes sense in that domain, which may be implemented by a string buffer.
모의객체주의자들은 역할 인터페이스(role interfaces) 를 좋아하고 모의객체 스타일이 역할 인터페이스를 장려한다고 단언한다. 왜냐하면 객체간 각각의 협력이 따로따로 모형화 될 수 있고 따라서 역할 인터페이스로 변경될 가능성이 더 크기 때문이다. 그러므로 보고용 문자열을 만드는 데에 문자열 버퍼를 사용하는 위 예제에서, 모의객체주의자는 그러한 도메인에 적합한 특수 역할을 고안할 가능성이 더 크고, 아마도 그것은 문자열 버퍼로 구현될 것이다.
It's important to remember that this difference in design style is a key motivator for most mockists. TDD's origins were a desire to get strong automatic regression testing that supported evolutionary design. Along the way its practitioners discovered that writing tests first made a significant improvement to the design process. Mockists have a strong idea of what kind of design is a good design and have developed mock libraries primarily to help people develop this design style.
설계 스타일에 있어 이런 차이가 대부분의 모의객체주의자들에게 중요한 동기가 된다는 점을 기억하는 것은 중요하다. TDD의 기원은 진화적 설계를 지원하는 강력한 자동 회귀 테스팅을 얻고자 하는 욕구로부터였다. 그 길을 따라 수련하는 사람들은 테스트를 먼저 작성하는 것이 설계 프로세스에 중대한 향상을 가져왔음을 발견했다. 모의객체주의자들은 어떤 설계가 좋은 설계인지에 대해 강력한 의견을 가지고 있는 사람들이고, 사람들이 이러한 설계 스타일을 발전시키기 위해 도움을 주기 위해 모의객체 라이브러리를 개발했다.
그래서 고전주의자가 되어야 하나요 아니면 모의객체주의자가 되어야 하나요? (So should I be a classicist or a mockist?)
I find this a difficult question to answer with confidence. Personally I've always been a old fashioned classic TDDer and thus far I don't see any reason to change. I don't see any compelling benefits for mockist TDD, and am concerned about the consequences of coupling tests to implementation.
자신있게 대답하긴 어려운 질문이라고 판단하고 있다. 개인적으로 나는 항상 예전 유행을 따르는 고전주의 TDD 개발자였고 지금까지 변화할 어떤 이유도 알지 못한다. 모의객체 TDD의 압도적인 장점도 보지 못하고, 현재는 테스트와 구현의 결합도에 따른 결과에 대해 걱정하는 사람이다.
This has particularly struck me when I've observed a mockist programmer. I really like the fact that while writing the test you focus on the result of the behavior, not how it's done. A mockist is constantly thinking about how the SUT is going to be implemented in order to write the expectations. This feels really unnatural to me.
내가 모의객체 프로그래머를 관찰했을 때 이 점이 특히 나를 놀래켰다. 나는 테스트를 작성할 때 어떻게 수행되는지가 아니라 행위의 결과에 초점을 맞춘다는 사실을 좋아한다. 모의객체주의자는 예측을 작성하기 위해 SUT가 어떻게 구현될지를 항상 생각한다. 내게는 이것이 매우 부자연스럽다.
I also suffer from the disadvantage of not trying mockist TDD on anything more than toys. As I've learned from Test Driven Development itself, it's often hard to judge a technique without trying it seriously. I do know many good developers who are very happy and convinced mockists. So although I'm still a convinced classicist, I'd rather present both arguments as fairly as I can so you can make your own mind up.
장난감 이상의 규모에 대해 모의객체 TDD를 시도해 보지 않아 점수를 더 많이 줄 수 없어 곤란하기도 하다. 나는 TDD를 TDD로부터 배웠기 때문에, 진지하게 시도해보지 않고 어떤 기법을 판단하는 것은 어렵다. 나는 모의객체에 대해 행복해하며 확신을 가진 훌륭한 개발자들을 알고 있다. 따라서 비록 내가 여전히 확신을 가진 고전주의자라 해도, 두가지 주장을 되도록 공정하게 제시하여 당신이 스스로 결론을 내리게 하려 한다.
So if mockist testing sounds appealing to you, I'd suggest giving it a try. It's particularly worth trying if you are having problems in some of the areas that mockist TDD is intended to improve. I see two main areas here. One is if you're spending a lot of time debugging when tests fail because they aren't breaking cleanly and telling you where the problem is. (You could also improve this by using classic TDD on finer-grained clusters.) The second area is if your objects don't contain enough behavior, mockist testing may encourage the development team to create more behavior rich objects.
그러므로 모의객체 테스팅이 당신에게 끌리면, 한 번 시도해 볼 것을 제안한다. 특히나 당신이 모의객체 TDD가 개선하고자 하는 몇 가지 부분에서 어려움을 겪고 있다면 시도할 만한 가치가 있다. 여기에는 두개의 주요 영역이 있다고 생각한다. 하나는 당신이 실패하는 테스트를 디버깅하느라 많은 시간을 보내고 있는데 그 실패가 깔끔하지 않고 문제가 어딘지 알려주지 않는 경우이다. 두번째는 객체가 충분히 행위를 가지고 있지 않아서, 모의객체 테스팅이 개발팀에게 행위가 더 풍부한 객체를 만들도록 장려할 수 있을 때이다.
최종 의견 (Final Thoughts)
As interest in unit testing, the xunit frameworks and Test Driven Development has grown, more and more people are running into mock objects. A lot of the time people learn a bit about the mock object frameworks, without fully understanding the mockist/classical divide that underpins them. Whichever side of that divide you lean on, I think it's useful to understand this difference in views. While you don't have to be a mockist to find the mock frameworks handy, it is useful to understand the thinking that guides many of the design decisions of the software.
유닛테스팅, xunit 프레임워크 그리고 테스트주도개발에 대한 관심이 커짐에 따라, 점점 더 많은 사람들이 모의객체를 사용하고 있다. 많은 시간을 들여 모의객체 프레임워크를 공부하지만, 사람들은 모의객체 프레임워크의 토대인 모의객체/고전주의의 구분을 제대로 이해하지 못한 상태다. 어느 쪽을 선택하든지 상관없이, 관점의 차이를 이해하는 것이 나는 유용하다고 생각한다. 모의객체 프레임워크가 유용한 걸 알았다고 해서 모의객체주의자가 될 필요는 없지만, 그 소프트웨어의 설계 결정을 이끄는 생각을 이해하는 것은 유용하다.
The purpose of this article was, and is, to point out these differences and to lay out the trade-offs between them. There is more to mockist thinking than I've had time to go into, particularly its consequences on design style. I hope that in the next few years we'll see more written on this and that will deepen our understanding of the fascinating consequences of writing tests before the code.
이 아티클의 원래 목적은 이러한 차이를 집어내고 그들 사이의 트레이드 오프를 나열해 보는 것이었고 그것은 지금 버전도 그러하다. 모의객체주의자들의 생각 속에는 특히 설계 스타일의 결과 측면에서 내가 생각한 것보다 더 많은 것들이 있다. 다가오는 몇 년간 이에 대해 더 많은 글을 보고 코딩전에 테스트부터 작성하는 것의 매혹적인 결과에 대한 우리의 이해를 더 심화할 수 있기를 희망한다.
더 읽어 볼 것 (Further Reading)
For a thorough overview of xunit testing practice, keep an eye out for Gerard Meszaros's forthcoming book (disclaimer: it's in my series). He also maintains a web site with the patterns from the book.
xunit 테스팅 관례에 대해 전체적으로 보려면, 곧 나올 Gerard Meszaros의 책을 주목하라 (참고: 내 시리즈 책이다). 그는 이 책에 나오는 패턴을 다루는 웹사이트 도 운영하고 있다.
To find out more about TDD, the first place to look is Kent's book.
TDD에 대해 더 알아보려면, 켄트의 책이 맨 먼저 볼 책이다.
To find out more about the mockist style of testing, the first place to look is the mockobjects.com site where Steve Freeman and Nat Price advocate the mockist point of view with papers and a worthwhile blog. In particular read the excellent OOPSLA paper. For more on Behavior Driven Development, a different offshoot of TDD that is very mockist in style, start with Dan North's introduction.
모의객체 스타일 테스팅에 대해 더 알아보려면, Steve Freeman 과 Nat Price가 논문과 가치 있는 블로그로써 그들의 주장을 펼치고 있는 mockobjects.com 을 우선 가보라. 특히 뛰어난 OOPSLA 논문을 읽어라. 스타일에서 매우 모의객체 중심 TDD의 한 분파인 행위주도개발에 대해서는 Dan North의 소개를 보라.
You can also find out more about these techniques by looking at the tool websites for jMock, nMock, EasyMock, and the .NET EasyMock. (There are other mock tools out there, don't consider this list to be complete.)
또, jMock, nMock, EasyMock, 그리고 .NET EasyMock 웹사이트에서 이러한 기술들에 대해서 더 많은 정보를 얻을 수 있다. (그 밖에 다른 툴들도 있다. 이 목록이 완전하다고 생각하지 말라.)
XP2000 saw the original mock objects paper, but it's rather outdated now.
XP2000 에서 최초의 모의객체 논문이 나왔는데 이제는 좀 오래되었다.
중요 개정 (Significant Revisions)
02 Jan 07: Split the original distinction of state-based versus interaction-based testing into two: state versus behavior verification and classic versus mockist TDD. I also made various vocabulary changes to bring it into line with Gerard Meszaros's book of xunit patterns.
2007년 1월 2일: 원래 상태기반테스팅과 상호동작기반 테스팅으로 구분했던 것을 두가지로 나누었다: 상태검증과 행위검증, 그리고 고전 TDD와 모의객체 TDD. 또 Gerard Meszaros의 xunit 패턴 책과 보조를 맞추기 위해 여러가지 용어변경도 하였다.
08 Jul 04: First published
2004년 7월 8일: 처음 발표함.