용어집 · 테스트 기초
테스트 더블
영어: Test double
다른 이름: test doubles, 목 객체, mock object, 테스트 스텁
정의
테스트에서 실제 의존성을 대신하는 더미, 스텁, 페이크, 스파이, 목 같은 객체로, 테스트 대상 코드를 빠르고 예측 가능하게 실행하게 합니다.
테스트 더블은 결제 게이트웨이, 데이터베이스, 시계, 이메일 서비스 같은 실제 의존성을 테스트가 제어할 수 있는 대상으로 바꿔 놓는 것입니다. Gerard Meszaros가 널리 알린 이 이름은 영화의 스턴트 대역(stunt double)에서 왔습니다. 테스트 더블을 쓰면 테스트가 빠르고 결정적(deterministic)이 되며, 타임아웃이나 카드 결제 거절처럼 실제로 재현하기 어려운 상황을 시뮬레이션할 수 있습니다.
다섯 가지 종류
| 유형 | 하는 일 | 예시 |
|---|---|---|
| 더미(Dummy) | 파라미터를 채우기 위해 전달될 뿐 사용되지 않음 | 메서드가 요구하지만 이 테스트에서는 전혀 건드리지 않는 null이나 빈 로거 |
| 스텁(Stub) | 호출에 미리 정해진 응답을 반환 | rates.getRate("EUR")가 항상 0.92를 반환 |
| 스파이(Spy) | 호출 방식까지 기록하는 스텁 | send()가 특정 주소로 한 번 호출되었음을 기록 |
| 목(Mock) | 기대 동작이 미리 프로그래밍되어 있고 테스트가 상호작용을 검증 | gateway.charge(4999)가 정확히 한 번 호출되었는지 검증 |
| 페이크(Fake) | 실제로 동작하지만 단순화된 구현 | HashMap 기반의 인메모리 저장소 |
일상적으로는 이 모든 것을 “목(mock)“이라고 부르고, Mockito 같은 라이브러리는 스텁, 스파이, 목 어느 역할도 할 수 있는 객체를 만듭니다. 그래도 구분을 알아 두면 도움이 되며, 특히 스텁과 목의 차이가 중요합니다. 스텁은 테스트 대상 코드에 입력을 공급하고(상태 검증), 목은 코드가 의존성으로 내보내는 출력을 확인합니다(행위 검증).
예시 (JUnit 5와 Mockito)
@Test
void declinedCardDoesNotSendConfirmation() {
PaymentGateway gateway = mock(PaymentGateway.class);
EmailService email = mock(EmailService.class);
when(gateway.charge(4999)).thenReturn(PaymentResult.DECLINED); // stub
var checkout = new Checkout(gateway, email);
var result = checkout.placeOrder(4999);
assertEquals(OrderStatus.PAYMENT_FAILED, result.status());
verify(email, never()).sendConfirmation(any()); // mock-style verification
}
(org.mockito.Mockito, org.mockito.ArgumentMatchers, org.junit.jupiter.api.Assertions의 static import는 생략했습니다.)
흔한 실수
- 모든 것을 목으로 만듭니다. 내부 호출을 하나하나 검증하는 테스트는 리팩터링할 때마다 깨지고, 실제 통합이 망가져 있어도 통과할 수 있습니다. 목은 아키텍처 경계에서 사용하고, 경계 안쪽에서는 실제 객체나 페이크를 선호합니다.
- 현실과 어긋난 테스트 더블. 실제 서비스에서 몇 달 전에 이름이 바뀐 필드를 여전히 반환하는 스텁 API는 잘못된 확신을 줍니다. 계약 테스트와 소수의 실제 통합 테스트가 테스트 더블을 정직하게 유지해 줍니다.
- 직접 소유하지 않은 타입을 목으로 만듭니다. 서드파티 클라이언트는 자체 인터페이스로 감싸고, 그 인터페이스를 테스트 더블로 대체합니다.