반응형
- 테스트 코드를 작성하면서 InMemoryRepository, FakeRepository, RepositoryStub 를 별다른 기준 없이 네이밍을 작성했다.
- 다른 개발자들과 의사소통을 위해서는 일반적으로 사용하는 네이밍 규칙을 따를 필요가 있다고 생각해 테스트 대역에 사용하는 용어들에 대해 정리
Test Double
- 우리말로 번역하면 테스트 대역이다. 여기서 대역은 대신 다른 역할을 하는걸 말한다.(잘 모를땐 주파수 대역을 떠올렸었다…)
- 테스트에서 실제 객체를 대신하는 가짜 객체
- 데이터베이스, 외부 API, 파일 시스템 등과 연결되어 있을 경우 테스트를 작성하기 어려워진다. 이 때 외부 의존성을 대신하는 객체를 만들어 테스트를 작성할 수 있다.
- 외부 의존성 없이 순수하게 테스트
- Test Double을 사용하면:
- 외부 의존성 없이 순수하게 테스트 대상 클래스의 로직만 검증할 수 있다.
- 예측 가능한 결과를 반환하도록 설정해 다양한 시나리오에 대한 테스트가 가능하다.
- 테스트 실행 속도를 크게 향상시킨다. - 직접 DB를 실행하는 것 보다 대역을5 사용한다.
종류
Test Double은 목적과 동작 방식에 따라 5가지로 구분할 수 있다.
Dummy
- 의존성은 있지만 해당 테스트에서는 사용하지 않는 객체가 필요할 때 사용한다.
@Test
void test01() {
// 실제로 사용되지 않는다.
NotificationService dummyNotification = null;
UserService userService = new UserService(userRepository, dummyNotification);
// 이 테스트에서는 알림 기능을 사용하지 않으므로 null이어도 문제없다.
User user = userService.createUser("user01@example.com");
assertThat(user.getEmail()).isEqualTo("user01@example.com");
}
Fake
- 실제 구현체보다 단순하지만 동일하게 동작하는 구현을 가진 객체
- 실제 외부 시스템 없이 동작하는 구현체가 필요할 때 사용(메모리 DB, 파일 시스템 등)
// Fake Repository - 메모리 기반으로 동작
public class FakeUserRepository implements UserRepository {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
@Override
public User save(User user) {
user.setId(idGenerator.incrementAndGet());
users.put(user.getId(), user);
return user;
}
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}
}
@Test
void test02() {
UserRepository fakeRepository = new FakeUserRepository();
UserService userService = new UserService(fakeRepository);
User savedUser = userService.createUser("user02@example.com");
User foundUser = userService.findById(savedUser.getId());
assertThat(foundUser.getEmail()).isEqualTo("user02@example.com");
}
Stub
- 테스트에 필요한 호출에 대해 미리 정해둔 응답만 반환하는 객체
- 특정 입력에 대한 예측 가능한 출력이 필요할 때 사용
@Test
void test03() {
// 미리 정해진 응답만 반환
ExternalApiClient stubApiClient = Mockito.mock(ExternalApiClient.class);
given(stubApiClient.getUserInfo(any())).willReturn(
new UserInfo("user01", "user01@example.com")
);
UserService userService = new UserService(stubApiClient);
UserInfo result = userService.fetchUserInfo("12345");
assertThat(result.getName()).isEqualTo("user01");
assertThat(result.getEmail()).isEqualTo("user01@example.com");
}
Spy
- Stub의 기능에 더해 메서드 호출 정보를 기록하는 객체
- 메서드가 올바르게 호출되었는지 확인하면서 실제 동작도 필요할 경우 사용
@Test
void test04() {
// Spy - 호출 내역을 기록
EmailService spyEmailService = Mockito.spy(new EmailService());
UserService userService = new UserService(userRepository, spyEmailService);
userService.registerUser("john@example.com");
// 이메일이 정확히 한 번 호출되었는지 확인
verify(spyEmailService, times(1)).sendWelcomeEmail("john@example.com");
}
Mock
- 호출 횟수, 파라미터, 호출 순서 등을 미리 설정하고 검증하는 객체
- 메서드 호출 여부, 횟수, 순서 등의 행위를 검증해야할 경우 사용
@Test
void test05() {
PaymentService mockPaymentService = mock(PaymentService.class);
OrderRepository mockOrderRepository = mock(OrderRepository.class);
// 실패 시나리오 설정
given(mockPaymentService.processPayment(any())).willThrow(new PaymentException());
OrderService orderService = new OrderService(mockOrderRepository, mockPaymentService);
assertThrows(PaymentException.class, () -> {
orderService.createOrder("item123", 100);
});
// 결제 실패 시 주문이 저장되지 않았는지 검증
verify(mockOrderRepository, never()).save(any(Order.class));
verify(mockPaymentService, times(1)).processPayment(any());
}
반응형