좋은 테스트 코드는 FIRST 규칙을 따릅니다:
Test Double은 "대역"을 의미합니다. 영화 촬영에서 스턴트 더블이 위험한 장면을 대신 연기하듯이, 테스트에서도 실제 객체를 대신하는 객체를 사용합니다.
단위 테스트를 진행할 때 생성하기 까다로운 클래스를 직접 생성(Sociable Test)하지 않고, 이를 대신해 테스트를 진행할 수 있도록 만들어진 객체를 Test Double(Solitary Test)이라고 합니다.
Test Double에는 크게 4가지 종류가 있습니다:
Stub은 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 객체입니다.
특징:
// Stub 예시
class EmailServiceStub implements EmailService {
@Override
public boolean send(String email) {
return true; // 항상 성공을 반환
}
}
Spy 객체는 관점에 따라 2가지로 정의됩니다.
Stub의 역할을 하면서 부가적인 정보를 기록하는 기능을 가진 객체를 의미합니다.
실제 객체를 감싸서 지정된 메서드만 Stub하고, 그렇지 않은 메서드는 Production Code처럼 동작하는 객체를 말합니다.
// Spy 예시
@Spy
UserService userService;
@Test
void spyExample() {
// 특정 메서드만 Stub
doReturn("Mocked Name").when(userService).getUserName();
// 나머지 메서드는 실제 코드 실행
userService.saveUser(user);
}
Mock은 실제 객체의 동작을 모방하는 객체입니다.
특징:
// Mock 예시
@Mock
EmailService emailService;
@Test
void mockExample() {
// given
when(emailService.send(anyString())).thenReturn(true);
// when
userService.registerUser(user);
// then - 행위 검증
verify(emailService, times(1)).send(user.getEmail());
}
// Stub - 상태 검증
@Test
void stubTest() {
// given
when(repository.findById(1L)).thenReturn(user);
// when
User result = userService.getUser(1L);
// then - 반환된 결과(상태)를 검증
assertThat(result.getName()).isEqualTo("John");
}
// Mock - 행위 검증
@Test
void mockTest() {
// when
userService.deleteUser(1L);
// then - 메서드가 호출되었는지(행위)를 검증
verify(repository, times(1)).deleteById(1L);
}
Test Double을 사용하는 이유는 간단합니다. 바로 주요 "관심사"에 집중하기 위해서입니다.
예를 들어, 사용자 등록 기능을 테스트한다고 가정해봅시다:
우리가 테스트하고 싶은 것은 사용자 등록 로직이 올바르게 동작하는지입니다. 외부 의존성은 Test Double로 대체하여 핵심 로직에만 집중할 수 있습니다.
Mockito는 Java에서 가장 많이 사용되는 Mock Framework입니다.
Mock 객체를 생성합니다.
@Mock
private UserRepository userRepository;
Spring Application Context에 Mock Bean을 등록합니다. (Spring Boot 테스트용)
@MockBean
private EmailService emailService;
특정 객체의 메서드를 선택적으로 Stubbing하고, 나머지 메서드는 Production Code처럼 동작하도록 합니다.
@Spy
private UserService userService;
@Test
void spyTest() {
// 특정 메서드만 Stub
doReturn(true).when(userService).isAdmin(anyLong());
// 나머지는 실제 코드 실행
userService.processUser(1L);
}
클래스가 필요로 하는 의존성과 맞는 Mock 객체들을 감지하여, 해당 클래스의 객체가 만들어질 때 Mock 객체를 자동으로 주입해주는 어노테이션입니다.
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService; // Mock 객체들이 자동 주입됨
@Test
void injectMocksTest() {
// userService는 이미 Mock들이 주입된 상태
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
userService.registerUser(user);
verify(emailService).send(user.getEmail());
}
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
}
// 메서드 호출 시 특정 값 반환
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// 예외 던지기
when(userRepository.save(any())).thenThrow(new RuntimeException());
// void 메서드 Stubbing
doNothing().when(emailService).send(anyString());
// 메서드가 1번 호출되었는지 검증
verify(userRepository, times(1)).save(user);
// 메서드가 호출되지 않았는지 검증
verify(emailService, never()).send(anyString());
// 메서드가 최소 n번 호출되었는지 검증
verify(userRepository, atLeast(1)).findById(anyLong());
@Mock, @Spy, @InjectMocks 어노테이션을 적절히 활용하세요