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

wlsh44·2023년 2월 16일
0

doReturn vs 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/thenReturnwhen() 입니다.

함수 호출 순서대로라면 저 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 인스턴스로 바꾸고 테스트를 해보겠습니다.

보이시는것과 같이 when/thenReturn 방식에서는 예외가 발생하여 테스트에 실패하였습니다. 좀 더 쉬운 이해를 위해 예시를 하나만 더 들어보겠습니다.

@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

profile
정리정리

0개의 댓글