1. Mockito란?
- Mockito는 Java 진영에서 가장 인기 있는 mocking 프레임워크입니다.
- 단위 테스트를 작성할 때 테스트 대상 코드가 의존하는 객체들을 가짜(mock)로 대체하여 테스트를 용이하게 만들어줍니다.
- 예를 들어, 데이터베이스나 외부 API에 의존하는 코드를 테스트할 때 실제 데이터베이스나 API 대신 Mockito로 만든 가짜 객체를 사용할 수 있습니다.
1.1 테스트 더블의 종류
- Mockito에서 제공하는 대표적인 테스트 더블은 다음과 같습니다
- Mock: 가짜 객체를 생성하여 그 객체의 모든 행동을 프로그래밍할 수 있습니다.
- Spy: 실제 객체를 감싸서 일부 메서드만 가짜로 대체할 수 있습니다.
- Stub: 미리 준비된 답변으로 메서드 호출에 응답하는 객체입니다.
2. Mockito 5 설정하기
2.1 의존성 추가
최신 Spring Boot 3.x 프로젝트를 사용한다면 별도의 설정 없이 spring-boot-starter-test
에 Mockito가 포함되어 있습니다. 하지만 수동으로 설정해야 하는 경우 다음과 같이 의존성을 추가합니다:
Maven의 경우
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
Gradle의 경우
testImplementation 'org.mockito:mockito-core:5.14.2'
2.2 JUnit 5와 통합
- Mockito 5는 JUnit 5와 원활하게 통합됩니다.
@ExtendWith(MockitoExtension.class)
를 사용하여 Mockito의 기능을 JUnit 테스트에서 사용할 수 있습니다:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
// 테스트 코드
}
3. Mock 객체 생성하기
Mock 객체를 생성하는 방법은 크게 두 가지가 있습니다.
3.1 애노테이션을 사용한 방법
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository; // Mock 객체 생성
@InjectMocks
private UserService userService; // Mock을 주입받는 대상
// 테스트 메서드...
}
@Mock
: Mock 객체를 생성합니다.@InjectMocks
:@Mock
으로 생성된 객체를 자동으로 주입받습니다.
3.2 직접 생성하는 방법
UserRepository userRepository = mock(UserRepository.class);
UserService userService = new UserService(userRepository);
- 이 방법은 테스트 메서드 내에서 지역적으로 Mock 객체가 필요할 때 유용합니다.
4. Stubbing: Mock 객체의 행동 정의하기
- Stubbing은 Mock 객체가 어떻게 동작해야 하는지 정의하는 것입니다.
4.1 기본적인 Stubbing
// Mock 객체의 메서드가 특정 값을 반환하도록 설정
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
// BDD 스타일로도 같은 동작을 정의할 수 있습니다
given(userRepository.findById(1L))
.willReturn(Optional.of(new User(1L, "John")));
4.2 여러 번 호출될 때의 동작
// 순차적으로 다른 값을 반환하도록 설정
when(userRepository.findById(1L))
.thenReturn(Optional.of(user1)) // 첫 번째 호출
.thenReturn(Optional.of(user2)) // 두 번째 호출
.thenThrow(new RuntimeException()); // 세 번째 호출
4.3 조건부 응답
// 메서드 호출 시 전달된 인자에 따라 다른 동작을 하도록 설정
when(userRepository.findById(anyLong()))
.thenAnswer(invocation -> {
Long id = invocation.getArgument(0);
if (id < 0) {
throw new IllegalArgumentException("Invalid ID");
}
return Optional.of(new User(id, "User " + id));
});
5. 검증(Verification)
- 검증은 Mock 객체의 메서드들이 예상대로 호출되었는지 확인하는 과 정입니다.
- Mockito에서는
verify
메서드를 사용하여 검증을 수행합니다. - 검증은 Mock 객체가 실제로 사용되는 방식을 확인하고 테스트 대상 코드의 동작을 검증하는 데 도움이 됩니다.
5.1 기본적인 검증
// userService.deleteUser(1L) 메서드를 호출한 후...
// deleteById가 정확히 한 번 호출되었는지 검증
verify(userRepository).deleteById(1L);
// 특정 횟수만큼 호출되었는지 검증
verify(userRepository, times(3)).findById(any());
// 전혀 호출되지 않았는지 검증
verify(userRepository, never()).save(any());
5.2 아규먼트 검증
- 아규먼트 검증은 Mock 객체의 메서드가 호출될 때 전달된 인자를 검증하는 것입니다.
- Mockito에서는
eq
,any
,notNull
,startsWith
등의 아규먼트 매처를 사용하여 검증할 수 있습니다.
기본 아규먼트 매처 사용하기
// 정확한 값과 매칭
verify(userRepository).findById(eq(1L));
// 어떤 타입의 값이든 매칭
verify(userRepository).save(any(User.class));
// null 여부로 매칭
verify(userRepository).findById(notNull());
// 문자열 패턴 매칭
verify(userRepository).findByUsername(startsWith("john"));
ArgumentCaptor를 사용한 상세 검증
ArgumentCaptor는 메서드 호출 시 전달된 인자를 캡처하여 나중에 검증할 수 있게 해줍니다:
@Test
void verifyUserUpdate() {
// ArgumentCaptor 준비
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
// 테스트 실행
userService.updateUser(1L, "John Doe", "john@example.com");
// 메서드 호출 시 전달된 User 객체 캡처
verify(userRepository).save(userCaptor.capture());
// 캡처한 User 객체의 속성 검증
User capturedUser = userCaptor.getValue();
assertThat(capturedUser.getName()).isEqualTo("John Doe");
assertThat(capturedUser.getEmail()).isEqualTo("john@example.com");
}
여러 아규먼트 동시 검증
@Test
void verifyMultipleArguments() {
// 테스트 실행
userService.updateUserRole(1L, "ADMIN");
// 여러 아규먼트를 한 번에 검증
verify(userRepository).saveUserAndRole(
argThat(user -> user.getId().equals(1L)),
argThat(role -> role.getName().equals("ADMIN"))
);
}
5.3 호출 순서 검증
특정 메서드들이 정해진 순서대로 호출되었는지 검증할 수 있습니다:
// 호출 순서 검증을 위한 InOrder 객체 생성
InOrder inOrder = inOrder(userRepository, emailService);
// 순서대로 검증
inOrder.verify(userRepository).save(any(User.class));
inOrder.verify(emailService).sendWelcomeEmail(any(String.class));
// 더 이상의 호출이 없었는지 확인
inOrder.verifyNoMoreInteractions();
6. 모범 사례와 주의사항
6.1 불필요한 Stubbing 피하기
테스트에 실제로 필요한 동작만 stub해야 합니다:
// 안 좋은 예 - 사용하지 않는 stub
when(userRepository.findById(any())).thenReturn(Optional.empty());
when(userRepository.save(any())).thenReturn(new User());
// 좋은 예 - 테스트에 필요한 것만 stub
when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "John")));
6.2 BDD 스타일 사용 권장
BDD(Behavior-Driven Development) 스타일을 사용하면 테스트의 의도를 더 명확하게 표현할 수 있습니다:
// Given (준비)
given(userRepository.findById(1L))
.willReturn(Optional.of(new User(1L, "John")));
// When (실행)
User user = userService.getUser(1L);
// Then (검증)
then(userRepository).should().findById(1L);
assertThat(user.getName()).isEqualTo("John");
6.3 Strict Stubbing 활용
Mockito 5는 기본적으로 strict stubbing을 사용합니다. 불필요한 stubbing을 방지하려면:
// 필요한 경우에만 lenient 모드 사용
@Mock(lenient = true)
private UserRepository userRepository;
// 또는 특정 stub에만 적용
lenient().when(userRepository.findById(any()))
.thenReturn(Optional.empty());
7. Mockito 5의 새로운 기능
Mockito 5에서는 다음과 같은 새로운 기능들이 추가되었습니다:
- 향상된 타입 안전성
- 제네릭 타입에 대한 더 나은 지원
- 컴파일 시점의 타입 체크 강화
- Java 17+ 지원
- 레코드 클래스 모킹 지원
- 봉인된 클래스와 인터페이스 지원
- 성능 개선
- 더 빠른 Mock 객체 생성
- 메모리 사용량 최적화