Mockito에는 stubbing을 하는 방법이 크게 2가지가 있습니다. 바로 doReturn/when
방식과 when/thenReturn
방식입니다. 두 방식 모두 거의 같은 기능을 제공하지만, 몇가지 차이점이 존재합니다. 예시를 보면서 하나씩 알아보겠습니다.
많은 사람들이 stubbing을 할 때 when/thenReturn
방식을 사용한다고 합니다. 이 방식이 좀 더 사람이 읽기 쉽게 적혀있어서 선호된다고 하네요. 하지만 이 방식은 void 메서드를 stubbing 하지 못한다는 단점이 존재합니다. 그래서 이럴 경우에는 doReturn/when
방식에서는 doNothing()
이라는 메서드를 지원하기 때문에 void 메서드의 stubbing이 가능해집니다.
다만 대부분의 경우 인터페이스가 되는 메서드는 리턴 값이 존재하기 때문에 크게 문제되는 일은 아닌 것 같습니다. 또한 필요할 때는 유연하게 doNothing
을 사용하면 될 것 같습니다. 사람들이 많이 사용한다고 그것이 코드의 정답이 되는 것은 아니니까요.
1번에서 when/thenReturn
의 단점에 대해 알아봤다면 2번은 doReturn/when
방식의 단점에 대해 알아보겠습니다. 사실 1번과 마찬가지로 크게 문제되는 부분은 아니라고 생각합니다. 단점은 위에 적어놓았듯이 doReturn/when
방법은 컴파일 단계에서 반환 타입을 체크하지 못한다는 점입니다. 예시를 보면 어떤 말인지 바로 알게 되실겁니다.
사진처럼 테스트가 실행은 되지만 stubbing 과정에서 mockito가 예외를 발생시킵니다. 하지만 stubbing을 할 때 테스트가 거의 무조건 깨지기 때문에 1번과 같이 크게 문제가 되는 부분은 아닌 것 같습니다.
마지막으로 살펴볼 차이점은 바로 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 으로 해뒀지만 사실 이번 포스트의 주제이기도 합니다.
위의 예시에서 예외가 발생하지 않았던 이유는 다들 아시겠지만 실제 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
방법을 사용하는 것을 권유하고 있습니다.
doReturn/when
계열의 방식을 사용하자.doReturn/when
방식은 컴파일 시 리턴 타입 체크가 되지 않는다.when/thenReturn
방식은 stubbing 중 메서드 호출이 이뤄지고, doReturn/when
은 이뤄지지 않는다.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