들어가며
어플리케이션 계층의 서비스 테스트 코드를 Mockito를 활용해 작성할 때, 아래와 같이 무수한 given절이 중복적으로 생기곤 한다.
이러한 given절 중 대다수는 테스트를 성공시키기 위한 부분이며, 실제로 검증하고자 하는 부분은 일부일 때가 많다.
테스트를 성공시키기 위한 given절들은 여러 테스트 메서드들에서 중복적으로 선언된다.
이렇게 given절이 많아지면, 해당 테스트 메서드에서 어떤 조건을 검증하려는지 파악하기 어려워진다.
따라서 나는 성공 조건들은 @BeforeEach문으로 분리하고, 실제로 검증하고자 하는 조건만 해당 테스트 메서드에 남기기로 했다.
given절을 @BeforeEach문으로 분리하기
우선 메서드 단위로 @Nested class를 구성한 후, 해당 클래스의 @BeforeEach 문에 성공 조건들을 모두 선언했다.
그리고 개별 테스트 메서드에서는 검증 조건 given절을 오버라이드했다.
개별 테스트 메서드에서 검증하고자 하는 조건을 한 눈에 파악할 수 있다.
하지만 이렇게 테스트를 실행하면 아래와 같이 UnnecessaryStubbingException이 발생한다.
UnnecessaryStubbingException은 Mockito에서 스터빙(Stubbing)된 메서드가 테스트 중 호출되지 않았을 때 발생한다.
쉽게 말해, given절을 선언했지만 해당 given절이 활용되지 않았을 때 발생한다.
이 예외의 발생 이유를 이해하기 위해서는 Mocktio의 Strictness 개념에 대해 알아야한다.
Mockito의 Strictness
Mockito의 Strictness는 테스트 중 모의 객체(Mock Object)의 호출에 대한 엄격한 검증 여부를 설정하는 기능이다.
(c.c: Mockito docs - Strictness)
Mockito는 LENIENT
, WARN
, STRICT_STUBS
의 3단계 Strictness를 가지고 있다.
LENIENT
: 관대한(lenient) 모드이다. 엄격한 검증을 수행하지 않는다.STRICT_STUBS
: 스터빙된 메서드가 호출되지 않았을 때 예외로 던진다.WARN
: 스터빙된 메세지가 호출되지 않았을 때 경고 메세지를 콘솔에 출력하되, 예외로 던지진 않는다.
Mockito 4이상부터는 STRICT_STUBS
가 default값이다. (이전까진 LENIENT가 default)
잘못된 스터빙을 했을 때, 실제 결과와 스터빙 결과를 혼동할 수 있다. STRICT_STUBS
모드는 사용자가 잘못된 스터빙을 했을 때 경고와 오류를 보고 파악할 수 있도록 해준다.
실제로 Mockito에서는 STRICT_STUBS
모드를 권장하지만, LENIENT
모드를 사용할 때의 장단점을 고려하고, 팀과 개인의 판단에 따라 선택하는 것을 허용한다.
실제로 이에 대한 논의가 stackoverflow에 있었다.
(c.c: stackoverflow - Mockito lenient() when to use)
I think it should be a personal or team judgement. Every case is different, so you can't simply say "you should not", but rather: "consider, but feel free to break this rule when desired". Following "the rules" blindly is as worse as not following them at all. Keep balance.
현재 테스트 메서드 내 너무 많은 given절로 가독성이 떨어지고, 서비스 코드에서 메서드 호출 순서가 변경되면 모든 테스트 메서드에서 given절을 수정해야 한다는 단점이 있다.
나는 이러한 단점을 보완하기 위해 @BeforeEach 구문에 제한적으로 LENIENT
모드를 사용하여 테스트 코드의 가독성을 향상시키기로 결정했다.
Strictness 모드 변경하기
@MockitoSettings
아래와 같이 @MockitoSettings 어노테이션을 활용해 클래스 단의 Strictness를 설정할 수 있다.
하지만 이 때, 해당 클래스의 모든 스터빙에 대해 LENIENT
모드가 적용됨을 주의해야한다. 테스트 메서드에 불필요한 스터빙이 존재해도 테스트가 통과한다.
LENIENT
모드에서의 부작용을 최대한 최대한 방지하기 위해서는, @BeforeEach 구문의 스터빙만 LENIENT
모드를 적용하고 개별 메서드에서는 STRICT_STUBS
모드를 사용하는 것이 좋다.
lenient()
lenient()를 사용하면 해당 스터빙에 LENIENT
모드를 적용할 수 있다.
하지만 lenient()에는 BDDMockito 문법이 적용되지 않는다.
BDDMockito 문법인 given-willReturn
대신 Mockito 문법 when-thenReturn
을 사용해야한다.
(Mockito 깃허브에서 lenient()에서 BDDMockito 문법 적용에 대한 몇차례 논의가 있었지만, PR이 몇년째 머지되지 않고 있다.)
그러나 우리팀은 BDDMockito를 활용하는 것이 컨벤션이다.
테스트 메서드에서는 BDDMockito 문법을 사용하고, @BeforeEach 구문에서는 Mockito 문법을 활용하는 데에는 불편함이 있었다.
읽는데도 부자연스럽고 코드 일관성이 저하되었으며, 특히 @BeforeEach 구문의 스터빙을 복사-붙여넣기하지 못한다는 점이 생산성을 저하시켰다.
나는 @BeforeEach 구문의 스터빙에 LENIENT 모드를 적용하되 BDDMockito 문법을 적용하기 위해 유틸리티 클래스를 정의했다.
Mockito 유틸리티 클래스 정의
아래는 내가 정의한 Mokcito 유틸리티 클래스이다.
LENIENT 모드를 적용할 때 활용가능한 유틸리티 클래스로, @BeforeEach 구문에서만 활용하기 위해 이름은 SetUpMockito
로 정했다.
public class SetUpMockito {
public static <T> Given<T> given(T methodCall) {
OngoingStubbing<T> ongoingStubbing = Mockito.lenient().when(methodCall);
return new Given<>(ongoingStubbing);
}
public static class Given<T> {
private final OngoingStubbing<T> ongoingStubbing;
public Given(OngoingStubbing<T> ongoingStubbing) {
this.ongoingStubbing = ongoingStubbing;
}
public void willReturn(T value) {
ongoingStubbing.thenReturn(value);
}
}
}
Mockito의 많은 메서드 중 현재 given절과 willReturn절만 활용하고 있기 때문에 이 두 메서드만 정의하였다.
추후에 다른 메서드를 활용하게 된다면 해당 메서드를 추가하면 된다.
이 유틸리티 클래스를 사용하면 BeforeEach 구문에서도 BDDMockito의 문법을 활용할 수 있어, 코드 가독성과 일관성을 향상시킬 수 있다.
아래는 SetUpMockito를 활용한 @BeforeEach 구문이다.
마무리
SetUpMockito를 정의함으로써 중복 given절을 없애고, 테스트 메서드의 가독성을 높일 수 있었다.
그러나 이를 무분별하게 사용하면 부적절한 스터빙으로 인해 테스트 결과에 혼동이 생길 수 있다.
나는 해당 유틸리티 클래스를 서비스 테스트의 @BeforeEach 구문에서만 사용해 부작용을 최소화하고자 한다.
또한, 서비스 코드에서 메서드 호출이 삭제되었을 때 @BeforeEach 구문에 불필요한 스터빙이 남을 수 있다는 점을 주의하며 사용해야 한다.
참고 자료
mockito blog - Clean tests produce clean code - strict stubbing in Mockito
'프로그래밍' 카테고리의 다른 글
AOP 및 @Retryable를 활용한 낙관적 락 재시도 (6) | 2023.11.16 |
---|---|
[Spring] REQUIRES_NEW와 데드락 위험성 (6) | 2023.10.13 |
[Spring] TaskScheduler를 활용해 런타임에 동적으로 작업 예약하기 (7) | 2023.10.08 |
Spring Event란? (0) | 2023.09.17 |
[Java] synchronized란? (1) | 2023.09.13 |