일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Hibernate
- 이펙티브 자바
- mockito
- MySQLTransactionRollbackException
- AWS
- oauth2.0
- Cannotacquirelockexception
- Cache
- OIDC
- N + 1
- Git
- 성능테스트
- @Transaction(readOnly=true)
- jdbc
- injellij
- Convention
- 데드락
- fetch join
- assert
- 정적 팩터리 메서드
- @controller
- batch insert
- 동시성
- spring-cloud-starter-aws
- @RequestMapping
- ngrinder
- JPA
- spring
- Batch
- awspring
- Today
- Total
정리정리
Mockito doReturn과 thenReturn 차이점 (feat. Spy) 본문
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 방법을 사용하는 것을 권유하고 있습니다.
정리
- void 메서드를 stubbing 하려면 doReturn/when 계열의 방식을 사용하자.
- doReturn/when 방식은 컴파일 시 리턴 타입 체크가 되지 않는다.
- when/thenReturn 방식은 stubbing 중 메서드 호출이 이뤄지고, doReturn/when은 이뤄지지 않는다.
- 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 |
---|