testing - stub, mock, spy 차이는?

    stub, mock, spy의 차이는?

    테스트 코드를 작성하다 보면, 한동안 Stub과 Mock의 차이를 생각하지 않고 써왔던 것 같다. Martin Fowler의 원본 글을 읽어보면서 내용을 정리해둬야 나중에 방향성을 잡을 때 도움이 될 것 같다.

     

    Mocks Aren't Stubs by Martin Fowler

    아래 링크가 원문이다.
    https://martinfowler.com/articles/mocksArentStubs.html

     

    용어 정리 + 개인 의견

    원문 글에도 자세히 나오지만, 원문에 소개된 개념을 정리한다.

    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 five particular kinds of double:
    
    * Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
    * 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).
    * 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.
    * 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.

    Test Double

    테스트계의 스턴트맨이라 생각하자. 영어에서는 Stunt Double이라고 부를 뿐이고, 한국사람들에게 더 익숙한 표현은 스턴트맨이다. 스턴트맨은 원래 배우 대신 위험을 무릅쓰고 대역을 하는 것이다. 대역을 쓰는 것에 착안하여 이름을 그렇게 지었고, 스턴트맨이 위험한 액션을 대역해 주는 것이라면, 테스트를 목적으로 대역을 쓰는 것이 Test Double이다.

    원본 객체야 물러나 있어. 테스트는 내가 대신 해줄께.

     

    Mock

    스마트폰을 전시해 놓고 파는 곳에 가보면, 몇몇 사람들이 말하는 목각폰이라고 말하기도 하는 Mock up이 존재한다. 즉, 가짜를 말한다. 실제와 동일한 기능을 하지는 않지만 대략 이렇게 생겼고, 크기는 이렇다, 대충 이런 기능이 이렇게 동작할 것이라고 알려주는 용도다. 테스트에서는, 호출시 동작이 잘 되었는지를 확인하는데 쓰인다.

     

    Stub

    Stub이라는 단어가 내포하는 바가, 전체 중 일부라는 뜻이다. 모든 기능 대신 일부 기능에 집중하여 임의로 구현한다. 일부 기능이라 함은 테스트를 하고자 하는 기능을 의미한다.

     

    Spy

    우리가 아는 스파이를 생각해 보자. 어딘가에 몰래 잠입하여, 뭔가 훔쳐본다. 그리고, 기록한다. 때로는 히든 미션을 수행하기도 한다. 어떨 때는 원래 그 소속인양 흉내내기도 한다. 그리고, 테스트계에도 이런 것이 있다.

     


     

    원문에서 비교하는 Stub기반 테스트와 Mock기반 테스트 비교

    Martin Fowler의 글에서 인용한 예제에 따르면,

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

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

    위에서 인용한 Stub기반의 코드는 상태기반 테스트이고, Mock기반의 테스트는 행위기반 테스트이다. 만약, Mock을 사용했지만 Stub의 코드와 같이 작성했다면, 사실 Stub기반 테스트 코드를 작성했던 것이라 할 수 있다.

     

    Spock으로 확인하기

    지극히 개인적인 생각인데 Mockito를 썼을 때는 Mock/ Stub의 차이가 잘 와 닿지 않는다. 시간이 지나면 Mock/ Stub의 목적을 잘 구분하지 않고, 그냥 테스트 코드를 작성하게 되는 것 같다. 반면, Spock을 썼을 때는 이 차이에 대해 좀더 생각해 보게 되는 것 같아서, Spock 기반으로 간단한 코드를 기록해 보고자 한다.

    다음과 같은 코드가 있을 때, Mock/Stub/Spy가 Spock에서는 어떻게 동작하는지 살펴보자.

    public class SampleService {
        private int result = 0;
    
        public int sum(int a, int b) {
            return a + b;
        }
    
        public int sum(List<Integer> values) {
            return values.stream().mapToInt(i -> i).sum();
        }
    
        public long multiply(int a, int b) {
            return a * b;
        }
    
        public float divide(int a, int b) {
            return a/b;
        }
    }

     

    Mock 테스트

    1) 다음 케이스는 정상 실행되지 않는다.

    def "testMock"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        mockSampleService.sum(testList) >> 55
    
        when:
        def result = mockSampleService.sum(testList)
    
        then:
        result == 55
        1 * mockSampleService.sum(_)
    }

    2) 다음 케이스는 정상 실행된다.

    def "testMock"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        mockSampleService.sum(testList) >> 55
    
        when:
        def result = mockSampleService.sum(testList)
    
        then:
        result == 55
    }

    3) 다음 케이스도 정상 실행된다.

    def "testMock"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        mockSampleService.sum(testList) >> 55
    
        when:
        def result = mockSampleService.sum(testList)
    
        then:
        1 * mockSampleService.sum(_)
    }

    앞서 Martin Fowler의 예제로부터, 2의 케이스가 Stub에 적합한 테스트 유형이지만 Mock으로도 Stub을 대행하여 사용할 수는 있다.

     

    Stub 테스트

    1) 다음 케이스는 정상 실행되지 않는다.

    def "testStub"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        stubSampleService.sum(testList) >> 55
    
        when:
        def result = stubSampleService.sum(testList)
    
        then:
        1 * stubSampleService.sum(_)
        result == 55
    }

    2) 다음 케이스도 정상 실행되지 않는다.

    def "testStub"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        stubSampleService.sum(testList) >> 55
    
        when:
        def result = stubSampleService.sum(testList)
    
        then:
        1 * stubSampleService.sum(_)
    }

    3) 다음 케이스는 정상 실행된다.

    def "testStub"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        stubSampleService.sum(testList) >> 55
    
        when:
        def result = stubSampleService.sum(testList)
    
        then:
        result == 55
    }

    Mock의 테스트와는 달리, Stub으로는 Stub의 케이스만 커버가 가능하다. 만약, Stub으로 Mock성 테스트를 진행하려고 했다면 다음과 같은 에러가 발생한다.

    Stub 'stubSampleService' matches the following required interaction:
    
    1 * stubSampleService.sum(_)   (0 invocations)
    
    Remove the cardinality (e.g. '1 *'), or turn the stub into a mock.

    즉, Mock이 Superset이긴 하지만, Mock과 Stub의 목적이 다르다는 것을 확인할 수 있다.

     

    Spy 테스트

    def "testSpy"() {
        given:
        def testList = [1,2,3,4,5,6,7,8,9,10]
        spySampleService.sum(testList) >> 55
    
        when:
        def result = spySampleService.sum(testList)
    
        then:
        /* working */
        1 * spySampleService.sum(_)
        result == 55ㅊ
    }

    Spy로는 위의 테스트 케이스가 통과한다. 이것이 시사하는 바는, 앞서 살펴본 다음 문장과 관계가 있지 않나 싶다.

    Spies are stubs that also record some information based on how they were called.

    즉, Spy는 위의 설명과 같이 일종의 Stub이면서 Mock에서 할 수 있는 행위 기반 테스트도 지원가능하다.

     

    반응형

     

    언제 Stub을 쓰고, 언제 Mock을 써야 하나?

    • Stub
      • 테스트의 입력에 집중하는가?
        • 그 입력값에 따라 리턴하는 결과값을 비교하는지?
        • 그 입력값에 따라 exception이 발생하는지?
    • Mock
      • 테스트의 출력/결과에 집중하는가?
        • 정상적으로 호출되었는지가 더 중요한지?

     

    참고

     

    Mocks, Fakes, Stubs and Dummies at XUnitPatterns.com

    Mocks, Fakes, Stubs and Dummies Are you confused about what someone means when they say "test stub" or "mock object"? Do you sometimes feel that the person you are talking to is using a very different definition? Well, you are not alone! The terminology ar

    xunitpatterns.com

     

    간단한 Spock 테스트 - 온라인에서 체험하기

    Javascript를 jsfiddle에서, SQL을 dbfiddle/sqlfiddle에서 확인하는 것처럼 Spock framework을 체험할 수 있는 사이트를 발견했다. 기존에 프로젝트가 셋업이 되어 있다면 그 프로젝트 파일에서 확인해 보면 좋

    luran.me

     

    SpringBoot + Spock 설정 방법

    본 글에서는 SpringBoot와 Spock Test Framework를 연동하는 방법에 대해 소개한다. 구성 환경 (의존성) SpringBoot: 2.4.2 Default JUnit: 5.x Spock: 1.3 Groovy: 2.5 만약, 이후에 다른 버전으로 테스트 한다면..

    luran.me

     

    댓글

    Designed by JB FACTORY