테스트 더블 (Test Double)
테스트 상황에서 ‘진짜 객체를 대신하는 가짜 객체’
<사용 이유>
- 테스트를 빠르게 만들 수 있음
- 외부 환경(DB, API 등)에 의존하지 않게 되어 안정적으로 테스트를 돌릴 수 있음
Test Double의 다섯 가지 유형
| 종류 | 역할 | 사용 목적 | 특징 |
| Dummy | 전달만 되고 사용되지 않음 | 생성자 인자 채우기용 | 호출되면 실패해도 됨 |
| Stub | 미리 정해진 응답 반환 | 테스트 제어, 분기 강제 | 질문하면 미리 짜둔 답변 |
| Spy | 호출 기록 + 실제 동작 | 호출 횟수, 인자 확인 | 실제 동작 수행 가능 |
| Mock | 기대 설정 + 검증 중심 | 상호작용(행위) 검증 | 누가 무엇을 몇 번 호출했는가 |
| Fake | 단순하지만 진짜처럼 동작 | 빠르고 결정적인 테스트 | 인메모리 DB, 로컬 캐시 등 |
Dummy
- 테스트 실행은 되도록 형식적으로 필요한 객체
- 실제로는 사용되지 않음
User user = new User(null, null); // 테스트에서 실제 사용되지 않음
Stub
- 미리 정해둔 값을 반환하게 만든 객체.
- 특정 상황을 강제
- 결제 실패, DB 예외, API 응답 오류 등 특정 상황을 인위적으로 만들 수 있다.
- 즉, 결과를 통제해서 비즈니스 로직의 분기와 예외 처리를 검증한다.
@Test
void stub_example_paymentFail() {
// mock()으로 만든 객체는 **“행위 검증(Mock)”에도, “상태 제어(Stub)”**에도 모두 쓸 수 있다.
// 즉, Mockito의 mock()은 도구일 뿐이고,
// **그걸 어떻게 쓰느냐(검증용 vs 제어용)에 따라 역할이 달라진다.**
**// [보조 Stub] : DB에서 정보를 가져오는 것처럼 인위적 상황 생성
// "해당 이메일이 아직 존재하지 않는다"는 상태
// 회원가입 로직이 이 조건을 타도록 강제한다.
// 즉, 외부 DB 의존성을 제거하고 테스트 환경을 통제하기 위한 보조 Stub이다.
UserRepository repo = mock(UserRepository.class);
given(repo.existsByEmail(any())).willReturn(false);**
**// [핵심 Stub] PaymentGateway는 실제 결제 API를 호출하지 않는다.
// 대신, "결제가 실패했다(false)"는 결과를 강제로 반환하도록 설정
// 테스트 목적의 중심이 되는 Stub
PaymentGateway stubPayment = mock(PaymentGateway.class);
given(stubPayment.charge(any(), anyInt())).willReturn(false); // 실패 상황 강제**
spy : 실제 호출을 수행하면서 그 동작의 기록을 남김
- 가짜(mock)처럼 검증이 가능하지만 실제 동작도 수행
- 실제 행동 + 검증가능성을 결합한 하이브리드 형태
- 호출 값, 호출 횟수, 파라미터 등을 기록하는 객체.
- 검증(verify)에 많이 사용됨
class SpyEmailService implements EmailService {
private int callCount = 0;
@Override
public void send(String email) {
callCount++;
}
public int getCallCount() {
return callCount;
}
}
mock : 올바른 협력 관계를 검증
- 행위 자체가 올바르게 일어났는가?
- stub: 상태 검증 / mock : 행동 검증
- 외부 시스템, 이벤트, 메시징, 알림 등 부수효과 검증시 필요
EmailService emailService = mock(EmailService.class);
// 동작 설정
when(emailService.send(any())).thenReturn(true);
// 호출 검증
verify(emailService, times(1)).send("test@test.com");
- Mock은 “로직 간 협력이 올바르게 이루어졌는가”를 검증
- 예시
- 결제 성공 후에만 이메일이 전송되는가
- 사용자 저장 전에 결제 시도가 일어나지 않았는가
- 외부 API가 한 번만 호출되었는가
fake : 실제처럼 동작하는 단순 대체 객체
- 인메모리 map
public class FakeUserRepository implements UserRepository {
private Map<Long, User> store = new HashMap<>();
@Override
public User save(User user) {
store.put(user.getId(), user);
return user;
}
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
언제 Mock을 사용해야 하고, 언제 실제 객체를 사용해야 하는가
| 구분 | Mock을 사용해야 하는 경우 | 실제 객체를 사용해야 하는 경우 |
| 의존성 존재 여부 | 외부 시스템(DB, API, 메시징 등)에 의존함 | 내부 계산, 도메인 규칙 등 독립 로직만 존재 |
| 테스트 목적 | “무엇을 호출했는가”를 검증하고 싶음 (행위 검증) | “무엇을 반환했는가”를 검증하고 싶음 (상태 검증) |
| 테스트 성격 | 협력 기반 테스트 (Service, Controller) | 순수 로직 테스트 (Entity, Value Object, Util) |
| 예시 | 결제 API 호출, 알림 발송, 이벤트 퍼블리시 | 포인트 계산, 할인 정책, 유효성 검사 |
정리
| 구분 | 목적 | 검증 대상 | 본질적 역할 | 대표 예시 |
| Dummy | 존재만 필요할 때 | 없음 | 테스트 노이즈 제거 | 사용 안 되는 의존성 |
| Stub | 결과를 제어하고 싶을 때 | 상태 변화(결제 실패 등) | 조건 제어, 분기 열기 | 결제 실패 예외 처리 |
| Spy | 부분 검증 | 호출 기록(횟수, 인자) | 실제 동작 + 관찰 | 메일 발송 기록 확인 |
| Mock | 행위 검증 | 호출 여부/순서 | 상호작용 보장 | 슬랙 알림, 외부 호출 |
| Fake | 단순 구현 | 실제처럼 동작하는 로직 | 현실 시뮬레이션 | 인메모리 Repository |
Mock vs Stub vs Spy
| 구분 | 핵심 관점 | 검증 대상 | Mockito 예시 | 요약 |
| Stub | 상태 제어 | “무엇을 반환했는가” | given(...).willReturn(...) | 특정 상황을 강제로 만듦 |
| Mock | 행위 검증 | “무엇을 호출했는가” | verify(...).methodCall() | 협력 관계 검증 |
| Spy | 실제 동작 + 기록 | “얼마나, 무엇을 호출했는가” | @Spy or 수동 구현 | 실제 동작을 하면서 추적 |
구분하는 법
- Stub: “이 상황을 재현하고 싶다”→ “상태 검증 중심”
- → 예: 결제 실패 상황을 만들어서(결과 고정) 예외 처리 확인
- Mock: “이 코드가 올바르게 협력했나?”→ “행위 검증 중심”
- → 예: 결제 성공 후 이메일이 발송되었는가
- Spy: “실제로 보내고, 보냈다는 것도 기록하고 싶다”→ “현실적 + 검증 가능”
- → 예: 실제 메일 발송 로직을 수행하면서 호출 횟수 확인
'TIL(Today I Learned)' 카테고리의 다른 글
| 250924 (수) 스프링부트 페이징 구현과 소셜 로그인 개념 (0) | 2025.09.24 |
|---|