정리정리

Mockito doReturn과 thenReturn 차이점 (feat. Spy) 본문

Test

Mockito doReturn과 thenReturn 차이점 (feat. Spy)

wlsh 2023. 3. 27. 21:51

doRetur와 thenReturn

Mockito에는 stubbing을 하는 방법이 크게 2가지가 있습니다. 바로 doReturn/when 방식과 when/thenReturn 방식입니다. 두 방식 모두 거의 같은 기능을 제공하지만, 몇 가지 차이점이 존재합니다. 예시를 보면서 하나씩 알아보겠습니다.

1. void 메서드 Stubbing

많은 사람들이 stubbing을 할 때 when/thenReturn 방식을 사용한다고 합니다. 이 방식이 좀 더 사람이 읽기 쉽게 적혀있어서 선호된다고 하네요. 하지만 이 방식은 void 메서드를 stubbing 하지 못한다는 단점이 존재합니다. 그래서 이럴 경우에는 doReturn/when 방식에서는 doNothing()이라는 메서드를 지원하기 때문에 void 메서드의 stubbing이 가능해집니다.

다만 대부분의 경우 인터페이스가 되는 메서드는 리턴 값이 존재하기 때문에 크게 문제되는 일은 아닌 것 같습니다. 또한 필요할 때는 유연하게 `doNothing`을 사용하면 될 것 같습니다. 사람들이 많이 사용한다고 그것이 코드의 정답이 되는 것은 아니니까요.

2. compile 시 반환 타입 체크

1번에서 when/thenReturn 의 단점에 대해 알아봤다면 2번은 doReturn/when 방식의 단점에 대해 알아보겠습니다. 사실 1번과 마찬가지로 크게 문제 되는 부분은 아니라고 생각합니다. 단점은 위에 적어놓았듯이 doReturn/when 방법은 컴파일 단계에서 반환 타입을 체크하지 못한다는 점입니다. 예시를 보면 어떤 말인지 바로 알게 되실 겁니다.

사진처럼 테스트가 실행은 되지만 stubbing 과정에서 mockito가 예외를 발생시킵니다. 하지만 stubbing을 할 때 테스트가 거의 무조건 깨지기 때문에 1번과 같이 크게 문제가 되는 부분은 아닌 것 같습니다.

3. stubbing 중 메서드 호출 여부

마지막으로 살펴볼 차이점은 바로 stubbing 과정 중 해당 메서드를 호출하는지에 대한 여부입니다. 먼저 두 방식의 코드를 살펴보겠습니다.

@Test
void whenThenReturn() throws Exception {
    //given
    Target target = mock(Target.class);
    when(target.getHello()).thenReturn("World!");

    //when
    String res = target.getHello();

    //then
    assertThat(res).isEqualTo("World!");
}

@Test
void doReturnWhen() throws Exception {
    //given
    Target target = mock(Target.class);
    doReturn("World!").when(target).getHello();

    //when
    String res = target.getHello();

    //then
    assertThat(res).isEqualTo("World!");
}

public static class Target {
    public String getHello() {
        throwException();
        return "Hello";
    }

    private void throwException() {
        throw new RuntimeException();
    }
}

두 테스트 모두 동일한 결과를 가져옵니다. 하지만 두 테스트는 stubbing을 하는 과정에서 실제로 getHello() 메서드를 호출했는지에 대한 차이점이 있습니다. 그리고 당연히 첫 번째 테스트인 when/thenReturn 테스트가 getHello() 메서드를 실제로 호출합니다.

생각해 보면 단순한 문제입니다. 함수 안에 함수가 있다면 내부에 있는 함수가 먼저 호출이 되는 건 당연한 일이니까요.

methodB(methodA()); // methodA() -> methodB()
when(target.getHello()); // getHello() -> when()

이런 코드가 존재한다고 생각하면 methodA()가 먼저 호출되고, 같은 이치로 첫 번째 테스트의 stubbing 과정에서 getHello()를 호출하게 됩니다.

메서드 호출 디버깅

조금만 더 두 차이를 살펴보겠습니다. 그러기 위해서 when()을 기준으로 디버깅을 해보겠습니다.

우선 when/thenReturn의 when()입니다. 

함수 호출 순서대로라면 저 methodCall은 mock 인스턴스의 getHello()의 리턴값이어야 합니다. 그리고 mock 인스턴스이기 때문에 결과는 예상대로 null 값을 가지고 있습니다. 여기서 재미있는 점은 인자로 넘어온 methodCall이 아무 데도 쓰이지 않는다는 점입니다. 이미 래핑 된 getHello()는 호출되는 순간에 Mockito의 인터셉터에 의해 stubbing 되는 메서드의 정보를 넘기는 방식이기 때문에 when() 안에서 딱히 사용될 일이 없습니다.

doReturn/when 방식은 이와는 조금 다릅니다. 우선 doReturn을 디버깅하다 보면 다음과 같은 코드를 볼 수 있습니다.

이곳에서 answers라는 리스트에 우리가 기대하는 결과를 미리 넣어두는 작업을 진행합니다. 그래서 파라미터에 doReturn("World!")에 넣어두었던 World!가 있는 것을 볼 수 있습니다. 이후 StubberImpl 자신을 리턴하여 체이닝을 하는 과정을 통해 when() 안에서 answers를 재사용합니다.

그리고는 when/thenReturn과 달리 이미 기대 값 설정이 끝난 mock 객체를 리턴해서 우리가 stubbing을 원하는 메서드를 선택하게 합니다. 그래서 다음과 같은 코드를 작성할 수 있는 것이죠.

doReturn("World!").when(target).getHello();

언뜻 보면 큰 차이는 아닌 것처럼 보이고, 실제로도 대부분의 경우 테스트가 동일한 결과를 가져옵니다. 하지만 mock 인스턴스를 만드는 게 아닌 spy 인스턴스를 만드는 경우라면 얘기가 달라집니다. 그리고 포스트 제목에는 feat으로 해뒀지만 사실 이번 포스트의 주제이기도 합니다.

spy 시 when/thenReturn 문제점

위의 예시에서 예외가 발생하지 않았던 이유는 다들 아시겠지만 실제 Target 인스턴스가 아닌 mock 인스턴스를 생성해서 Target을 래핑 한 프록시 객체의 getHello() 가 호출되었기 때문입니다. 그렇다면 래핑 된 메서드를 사용하는 mock이 아니라 실제 메서드를 사용하는 spy라면 어떨까요?

mock 인스턴스를 spy 인스턴스로 바꾸고 테스트를 해보겠습니다.

보이시는 것과 같이 동일하게 stubbing을 했음에도 when/thenReturn 방식에서는 예외가 발생하여 테스트에 실패하였습니다. 정확히는 stubbing이 되지 않은 것이죠.

좀 더 쉬운 이해를 위해 예시를 하나만 더 들어보겠습니다.

@Test
void listSpy_whenThenReturn() throws Exception {
    //given
    List spy = spy(LinkedList.class);
    when(spy.get(0)).thenReturn("foo");

    //when
    Object res = spy.get(0);

    //then
    assertThat(res).isEqualTo("foo");
}

@Test
void listSpy_doReturnWhen() throws Exception {
    //given
    List spy = spy(LinkedList.class);
    doReturn("foo").when(spy).get(0);

    //when
    Object res = spy.get(0);

    //then
    assertThat(res).isEqualTo("foo");
}

리스트의 첫 번째 원소를 가져오는 메서드에 stubbing을 하는 테스트 코드입니다.

결과는 예상대로 when/thenReturn에서만 예외가 발생하여 테스트가 깨졌습니다.

왜 이런 일이 발생하는 걸까요? 3번의 차이점을 이해하셨다면 금방 알아채셨을 겁니다. 차이점은 바로 stubbing 과정 중 메서드 호출의 여부에 있습니다. 위의 사진 중 인텔리제이가 예외가 발생했다고 알려주는 부분을 보면 when(spy.get(0)).thenReturn("foo");  get에 밑줄이 그어져 있는 것을 알 수 있습니다.

spy는 mock처럼 위임된 메서드를 호출하는 것이 아니라 실제 인스턴스의 메서드를 호출합니다. 그렇기 때문에 stubbing 과정 중 실제로 spy.get(0)이 호출이 되었고, 비어있는 리스트의 인덱스를 참조했기 때문에 IndexOutOfBoundsException이 발생했습니다.

사실 이 예제는 spying 할 때 조심할 점으로 Mockito의 docs에 나와있는 내용입니다. (docs)
그래서 docs에서도 when/thenReturn 대신 doReturn/when 방법을 사용하는 것을 권유하고 있습니다.

정리

  1. void 메서드를 stubbing 하려면 doReturn/when 계열의 방식을 사용하자.
  2. doReturn/when 방식은 컴파일 시 리턴 타입 체크가 되지 않는다.
  3. when/thenReturn 방식은 stubbing 중 메서드 호출이 이뤄지고, doReturn/when은 이뤄지지 않는다.
  4. spy 인스턴스를 사용할 때는 doReturn/when 를 고려하자.

참고

https://www.javadoc.io/doc/org.mockito/mockito-core/1.10.19/org/mockito/Mockito.html#13

https://stackoverflow.com/questions/20353846/mockito-difference-between-doreturn-and-when
https://stackoverflow.com/questions/11620103/mockito-trying-to-spy-on-method-is-calling-the-original-method

'Test' 카테고리의 다른 글

Spring Service 계층 테스트와 데이터 초기화  (0) 2023.03.20
Comments