본문 바로가기

TDD - 테스트 더블(Test Double)

@6uiw2025. 11. 17. 20:38

 

테스트 더블 (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: “실제로 보내고, 보냈다는 것도 기록하고 싶다”→ “현실적 + 검증 가능”
  • → 예: 실제 메일 발송 로직을 수행하면서 호출 횟수 확인
6uiw
@6uiw :: LOG.INFO("MING's DEVLOG")

개발을 하면서 공부한 기록을 남깁니다

목차