본문으로 건너뛰기
중급25

목·스텁·스파이: 테스트 더블의 종류와 쓰임

진짜 이메일 서비스나 결제 게이트웨이 없이도 테스트할 수 있는 이유는 가짜 대역, 테스트 더블 덕분이다. 더미·스텁·스파이·목·페이크를 구분한다.

  • #테스트더블
  • #
  • #스텁
  • #스파이
  • #Mock

개념

테스트 더블(Test Double)이란

영화 촬영에서 위험한 장면을 대신하는 "스턴트 더블"처럼, 테스트에서 실제 의존성(이메일 서비스, 결제 게이트웨이, 데이터베이스 등)을 대신하는 가짜 객체를 테스트 더블이라 부른다. 모듈 9(레슨 9)에서 배운 "테스트 용이성이 높은 코드"는, 바로 이 더블로 바꿔 끼울 수 있게 설계된 코드였다.

왜 진짜 대신 가짜를 쓰는가

레슨 1의 단위 테스트는 빠르고, 독립적이고, 예측 가능해야 한다. 진짜 이메일 서비스를 쓰면 테스트할 때마다 실제 이메일이 발송되고 (느리고 위험함), 진짜 결제 게이트웨이를 쓰면 실제 돈이 오갈 수도 있다. 테스트 더블은 이런 위험 없이, 원하는 상황을 정확히 재현할 수 있게 해준다.

다섯 가지 테스트 더블

1. 더미(Dummy) — 그냥 자리만 채우는 값. 실제로 쓰이지는 않는다.

sendEmail(user, dummyLogger)  // dummyLogger는 호출되지만 아무 동작도 안 함

2. 스텁(Stub) — 미리 정해둔 값을 그대로 돌려주는 가짜.

const stubPaymentGateway = {
  charge: () => ({ success: true })  // 항상 "성공했다"고 답하도록 고정
}

3. 스파이(Spy) — 진짜처럼 동작하면서, 호출 여부와 방법을 기록해두는 가짜. "이 함수가 정확히 한 번, 이 값으로 호출됐는가"를 나중에 확인할 수 있다.

const emailSpy = spy(realEmailService.send)
sendWelcomeEmail(user, emailSpy)
expect(emailSpy).toHaveBeenCalledWith(user.email, "환영합니다")

4. 목(Mock) — 스파이와 비슷하지만, 미리 "이렇게 호출될 것이다"라는 기대를 설정해두고, 그 기대와 다르게 동작하면 테스트 자체가 실패하도록 만드는 더 엄격한 형태다.

5. 페이크(Fake) — 진짜처럼 동작하지만 훨씬 단순화된 구현. 예를 들어 진짜 데이터베이스 대신, 메모리에 데이터를 저장하는 간단한 가짜 데이터베이스를 만들어 쓰는 경우다.

실무에서는 용어보다 목적이 중요하다

다섯 가지를 완벽히 암기할 필요는 없다 — 실무에서는 이 용어들이 섞여서 쓰이기도 한다. 중요한 건 **"이 테스트 더블이 정해진 값만 돌려주는가(스텁), 아니면 호출 여부까지 확인하는가(스파이/목)"**를 구분하는 감각이다.

통합 테스트에서는 진짜를 더 많이 쓴다

레슨 1에서 배운 것처럼, 단위 테스트는 더블을 적극적으로 쓰지만, 통합 테스트는 반대로 실제(또는 실제에 가까운) 의존성을 그대로 써서 진짜 상호작용을 확인하는 것이 목적이다. 모듈 7(레슨 2)에서 배운 테스트 데이터·환경 관리가 바로 이 통합 테스트를 위한 준비 작업이다.

실무에서 왜 필요한가

QA가 "이 테스트는 진짜 결제 게이트웨이를 쓰나요, 아니면 가짜를 쓰나요?"라고 물어볼 수 있으면, 그 테스트가 실제로 무엇을 보장하는지 정확히 판단할 수 있다. 스텁으로 "항상 성공"만 흉내 낸 테스트는 결제 실패 시나리오를 전혀 검증하지 못한다 — 이런 빈틈은 테스트 더블의 종류를 이해하는 사람만 짚어낼 수 있다.

실습 과제

과제 1 — 더블 종류 구분하기 (15분)

다음 세 코드가 스텁·스파이·페이크 중 무엇에 해당하는지 판단한다.

// A
const fakeUserRepository = {
  users: [],
  save(user) { this.users.push(user) },
  findById(id) { return this.users.find(u => u.id === id) }
}
 
// B
const stubWeatherApi = { getTemperature: () => 25 }
 
// C
const notificationTracker = trackCalls(realNotificationService.notify)
// 나중에 notificationTracker.calls로 호출 기록 확인 가능

과제 2 — 빈틈 찾기 (10분)

"결제 게이트웨이를 항상 성공만 반환하는 스텁으로 대체해서 결제 테스트를 전부 통과시켰다"는 상황에서, 이 테스트 스위트가 놓치고 있을 가능성이 높은 시나리오를 2가지 적는다.

자가 체크리스트

  • 테스트 더블이 왜 필요한지 설명할 수 있다
  • 더미·스텁·스파이·목·페이크의 차이를 예시로 설명할 수 있다
  • 스텁만 쓴 테스트가 놓칠 수 있는 시나리오를 판단할 수 있다
  • 단위 테스트와 통합 테스트에서 더블을 쓰는 정도가 왜 다른지 설명할 수 있다

흔한 실수

  • 다섯 가지 용어를 완벽히 암기하는 데 집중하고, 실제 목적(빠르고 예측 가능한 테스트를 위한 대체)은 놓친다.
  • 스텁으로 항상 성공만 반환하게 만들어놓고, 실패 시나리오는 전혀 테스트하지 않는다. 실패·예외 상황도 스텁으로 재현해야 한다.
  • 통합 테스트에서도 모든 의존성을 더블로 바꿔버린다. 그러면 통합 테스트가 사실상 단위 테스트가 되어, 실제 연동 문제를 못 잡는다.

참고 자료

  • Martin Fowler — Mocks Aren't Stubs — 테스트 더블 용어를 정리한 대표적인 아티클
  • Gerard Meszaros, xUnit Test Patterns — 테스트 더블 패턴을 체계적으로 정리한 참고서