[Spring] JUnit5 + Mockito 단위테스트 작성
Review 도메인 개발을 담당하게 되면서, 모든 도메인이 구현되어야 최종 기능 테스트가 가능하다는 점에 불안한 마음이 들었다. 테스트 없이 다른 도메인이 완성되길 기다렸다간 프로젝트 후반부에 시간 확보에 있어 큰 문제가 될 수도 있겠다고 생각했다. 찾아보니 Mockito 라이브러리를 활용하면 단위테스트 작성을 보다 유연하게 진행할 수 있을 것 같아서, Spring + JUnit5 + Mockito 기반의 테스트를 작성해보았다.
Mockito
Java 기반 오픈소스 테스트 프레임워크로, 모의 객체(Mock Object)를 생성하고 관리하는 데 사용된다. 개발자가 모의 객체의 행위를 정의함으로써, 의존 객체로부터 독립적인 단위테스트를 수행할 수 있도록 돕는다.
Spring + JUnit5 + Mockito 사용법
1. Mockito 사용 설정
SpringBoot를 사용하면, spring-boot-starter-test에 Mockito 라이브러리가 포함되어 있다. build.gradle dependencies에 의존성을 추가한다.
dependencies {
...
testImplementation 'org.springframework.boot:spring-boot-starter-test'
...
}
Mockito를 사용하고자 하는 Test 클래스에도 애노테이션을 명시해줘야 한다.
@ExtendWith(MockitoExtension.class)
class ReviewServiceTest {
...
}
2. Mock 객체 의존성 주입
Mockito에서 모의 객체의 의존성 주입을 위해서는 3가지 애노테이션이 사용된다.
- @Mock: 모의 객체를 만들어 반환해주는 애노테이션
- @Spy: Stub하지 않은 메서드들은 원본 메서드 그대로 사용하는 애노테이션
- @InjectMocks: @Mock 또는 @Spy로 생성된 모의 객체를 자동으로 주입하는 애노테이션
// Stub 지정 필요, 정의한 동작대로 실행
@Mock
private ReviewRepository reviewRepository;
@InjectMocks
private ReviewService reviewService;
// Stub 지정 시 정의한 동작대로 실행, 미지정 시 원본 동작대로 실행
@Spy
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Mock 또는 @Spy로 모의 객체를 생성하고, @InjectMocks로 주입한다.
3. Stub로 결과 처리
생성된 Mock 객체의 예상 동작을 직접 정의하고 관리하는 부분으로, 3가지 Stubbing이 있다.
- Return: 특정 파라미터를 입력받았을 때 지정한 값을 리턴
- Exception Throwing: 특정 파라미터를 입력받았을 때 지정한 예외를 리턴
- Consecutive Calls: 메서드가 동일한 매개변수로 여러 번 호출될 때 각기 다른 행동을 하도록 조작
// Return
when(reviewRepository.findById(reviewId)).thenReturn(Optional.empty());
// Exception Throwing
when(reviewRepository.findById(reviewId)).thenThrow(new BusinessException());
// Consecutive Calls
when(reviewRepository.findById(reviewId))
.thenReturn(Optional.of(review))
.thenThrow(new BusinessException())
.thenReturn(Optional.empty());
4. Verify로 테스트 검증
Mock 객체의 메서드 호출을 확인하는 단계로, 메서드 호출 횟수를 검증할 수 있다.
// delete(review) 메서드 1번 호출 검증
verify(reviewRepository, times(1)).delete(review);
단위테스트 예시 (BDD style)
실제 내가 작성한 단위테스트 코드를 보자.
@InjectMocks
private ReviewService reviewService;
@Mock
private ReviewRepository reviewRepository;
@Mock
private MemberRepository memberRepository;
ReviewService 객체에서 필요로 하는 Repository 객체들을 Mock 객체로 생성해서 주입하도록 했다.
@BeforeEach
void setUp() {
...
member = mock(Member.class);
lenient().when(member.getMemberId()).thenReturn(1L);
lenient().when(member.getUsername()).thenReturn("testUser");
lenient().when(member.getNickname()).thenReturn("test nickname");
lenient().when(member.getEmail()).thenReturn("test@gmail.com");
lenient().when(member.getAddress()).thenReturn(address);
...
}
@BeforeEach 애노테이션으로 각 테스트 메서드가 실행되기 전, Mock객체의 동작을 지정하는 Stub 과정이 진행되도록 구현했다. 위 코드에서는 member를 @Mock으로 생성하고, 원하는 동작을 지정했다.
lenient()
중복되는 given 절을 없애는 데 사용되는 방법이다. 처음에 lenient() 없이 테스트를 진행하다보니, Mock 객체에 대한 설정(Stubbing)이 실제 테스트 메서드에서 호출되지 않을 때 발생하는 경고인 Unnecessary Stubbing 오류가 출력되었다.
나는 setUp() 메서드에 모든 테스트 메서드에서 사용할 Mock 객체들을 미리 정의해두고 재활용하려 했는데, 간혹 호출되지 않는 Stubbing 때문에 테스트가 실패하는 경우가 있었다.
Mockito의 lenient()를 사용하여 필요하지 않은 Stubbing으로 인한 테스트 실패를 피할 수 있다고 한다. 각 테스트 메서드마다 lenient() 없이 사용할 Mock 객체에 대한 Stubbing만 작성하는 방법도 있지만, 관리해야 할 객체가 많아져 코드가 복잡해지는 문제로 lenient()를 사용했다.
@Test
@DisplayName("리뷰 수정 예외 테스트")
void updateReviewTest_reviewNotFound() {
// given
when(reviewRepository.findById(reviewId)).thenReturn(Optional.empty());
// when & then
BusinessException exception = assertThrows(BusinessException.class,
() -> reviewService.updateReview(reviewId, "updated review", 4.3));
assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.REVIEW_NOT_FOUND);
}
이 코드는 리뷰를 수정할 때 리뷰를 찾을 수 없다는 예외가 발생하는 지 확인하는 테스트 관련 코드이다. setUp() 에서 설정해 준 reviewId 값을 넣어 review를 검색하면 Optional.empty()를 반환하도록 했다. 커스텀 예외인 BusinessException이 발생하는 지 검증한 후, 예외 코드를 확인해 의도한 예외가 생성되었음을 확인한다.
@Test
@DisplayName("리뷰 삭제 테스트")
void deleteReviewTest_success() {
// given
when(reviewRepository.findById(reviewId)).thenReturn(Optional.of(review));
// when
reviewService.deleteReview(reviewId);
// then
verify(reviewRepository, times(1)).delete(review);
}
엔티티 삭제를 테스트하기 위해서, 메서드 호출 횟수를 검증하도록 했다. delete(review)가 한 번 호출되었는 지 확인한다.