콘텐츠로 이동

Test Double

테스트 더블은 테스트를 위해 실제 객체를 대체하는 객체를 말한다.

  • 의존성 격리: 테스트하고자하는 객체를 외부 의존성으로 격리하여, 테스트 대상에만 집중할 수 있도록 함
  • 테스트 속도 향상: 외부 시스템과의 통신 혹은 복잡한 로직을 대체하여 테스트 속도를 향상
  • 경계 조건 테스트: 정상적인 환경 뿐만 아니라, 예외 상황에 대한 테스트도 가능
  • 비용 절감: 비용이 발생하는 외부 서비스나 리소스를 대체하여 테스트 비용을 절감
  • 안정적인 테스트 환경 제공: 외부 서비스의 가용성이나 변동에 관계 없이 안정적인 테스트 환경 구축

테스트 더블은 다음과 같이 종류가 나뉘며, 용도와 목적에 따라 적절한 테스트 더블을 사용해야 한다.

Test Double설명용도예시
Dummy전달되지만 실제로는 사용되지 않는 객체(파라미터 자리 채움)시그니처상 인스턴스가 필요하지만 해당 테스트 경로에서 호출되지 않는 경우검증 경로에서 호출되지 않는 Logger 등 의존성 자리 채움
Fake단순화 된 구현체로 동일한 기능은 수행하나, 프로덕션에선 부족한 객체비슷한 방식으로 동작하는 환경이 필요한 경우DB 대신 메모리에 저장하는 객체
Stub특정 상황에 대한 미리 정의된 결과를 반환하는 객체항상 같은 결과(=상태)를 반환해야 하는 경우외부 API 호출에서 항상 성공 응답을 반환하는 객체
SpyStub과 유사하나, 호출된 메서드에 대한 정보를 기록하는 객체호출된 횟수나 인자값을 확인해야 하는 경우이메일 발송 서비스에서 발송 메서드가 호출된 횟수와 실제 발송된 이메일 주소를 확인하는 객체
Mock특정 메서드 호출에 대한 기대를 명세하고, 검증하는 객체특정 인자값이나 횟수(=행동)를 검증해야 하는 경우결제 서비스에서 결제 처리 메서드가 특정 조건에서 호출되었는지 검증하는 객체
public class DummyLogger {
public void log(String message) {
// 아무 동작도 하지 않음
}
}
@Test
public void dummyTest() {
// MyService 생성자가 Logger를 요구하지만, 아래 검증 경로에서는 로깅이 일어나지 않음
DummyLogger dummyLogger = new DummyLogger();
MyService service = new MyService(dummyLogger);
service.calculateTotal(); // dummyLogger는 자리만 채울 뿐, log()는 호출되지 않음
}
public class FakeDatabase implements Database {
private Map<String, String> data = new HashMap<>();
@Override
public void save(String key, String value) {
data.put(key, value);
}
@Override
public String find(String key) {
return data.get(key);
}
}
@Test
public void fakeTest() {
Database fakeDb = new FakeDatabase();
fakeDb.save("username", "test_user");
assertEquals("test_user", fakeDb.find("username"));
}
public class PaymentGatewayStub extends PaymentGateway {
@Override
public PaymentResponse processPayment(double amount) {
// 항상 성공적인 결제를 반환하는 Stub
return new PaymentResponse(true, "Payment processed successfully");
}
}
@Test
public void processOrderWithStub() {
PaymentGatewayStub paymentGatewayStub = new PaymentGatewayStub();
OrderService orderService = new OrderService(paymentGatewayStub);
Order order = new Order(100.00);
boolean result = orderService.processOrder(order);
// 상태를 검증: 결제가 성공했는지 확인
assertTrue(result);
assertTrue(order.isPaymentSuccessful());
}
public class EmailServiceSpy extends EmailService {
private int sendEmailCallCount = 0;
private String lastRecipient = null;
@Override
public void sendEmail(String recipient, String message) {
sendEmailCallCount++;
lastRecipient = recipient;
super.sendEmail(recipient, message);
}
public int getSendEmailCallCount() {
return sendEmailCallCount;
}
public String getLastRecipient() {
return lastRecipient;
}
}
@Test
public void spyTest() {
EmailServiceSpy emailServiceSpy = new EmailServiceSpy();
MyService service = new MyService(emailServiceSpy);
service.notifyUser("user@example.com");
assertEquals(1, emailServiceSpy.getSendEmailCallCount());
assertEquals("user@example.com", emailServiceSpy.getLastRecipient());
}
@Test
public void processOrderWithMock() {
PaymentGateway mockPaymentGateway = mock(PaymentGateway.class);
OrderService orderService = new OrderService(mockPaymentGateway);
Order order = new Order(100.00);
// Mock 동작 설정: 결제가 성공했다고 가정
when(mockPaymentGateway.processPayment(order.getAmount()))
.thenReturn(new PaymentResponse(true, "Payment processed successfully"));
boolean result = orderService.processOrder(order);
// 행위를 검증: 결제 메서드가 올바르게 호출되었는지 확인
verify(mockPaymentGateway).processPayment(order.getAmount());
assertTrue(result);
assertTrue(order.isPaymentSuccessful());
}

마지막 업데이트:

Test