← Posts

Testing · Test Double · Mock

테스트 더블 - 진짜 대신 가짜를 끼운다

단위 테스트를 빠르게 하려면 진짜 DB·외부 API를 떼고 가짜로 갈아끼워야 한다. dummy·stub·spy·mock·fake가 어떻게 다른지, 상태 검증과 행위 검증의 차이, 과한 목의 함정까지.

목차
  1. 진짜 의존성이 걸림돌이다
  2. 그래서 가짜로 갈아끼운다
  3. 더블에도 종류가 있다
  4. 스텁: 정해진 답을 준다
  5. 목: 불렸는지를 확인한다
  6. 페이크: 진짜처럼 동작하는 가벼운 구현
  7. 과하게 끼우면 테스트가 거짓말한다
  8. 실무에서: 라이브러리와 판단
  9. 정리

단위 테스트가 빠른 건 조각 하나만 보기 때문이다. 그런데 그 조각에 진짜 의존성이 딸려 오면 빠름이 무너진다. 그걸 떼는 도구가 테스트 더블이다.

진짜 의존성이 걸림돌이다

주문 서비스의 “재고가 모자라면 거부한다”만 확인하고 싶다. 그런데 이 서비스는 재고 DB를 부르고, 결제 API를 호출하고, 알림 메일을 보낸다.

plaintext
주문 서비스  →  재고 DB (띄워야 함)
            →  결제 API (진짜 결제되면 곤란)
            →  메일 발송 (테스트마다 메일이?)

로직 하나 보려는데 진짜를 다 붙이면 느리고, 불안정하고(외부가 죽으면 테스트도 실패), 부작용까지 난다(진짜 결제·진짜 메일). 확인하려는 건 재고 규칙 하나뿐인데.

그래서 가짜로 갈아끼운다

영화의 스턴트 대역처럼, 진짜 의존성 자리에 가짜를 끼운다. 이 가짜들을 통틀어 테스트 더블(test double)이라 한다.

갈아끼우는 게 가능한 건 의존성 역전 덕이다. 서비스가 구체 클래스가 아니라 인터페이스에 기대고 있으면, 운영에선 진짜를, 테스트에선 가짜를 주입하면 된다. 리포지토리 패턴 글이 바로 이 교체를 구체 코드로 보여준다.

더블에도 종류가 있다

“가짜”라고 다 같지 않다. 무엇을 하느냐에 따라 다섯으로 나눈다(Gerard Meszaros의 분류).

종류하는 일
더미(dummy)자리만 채운다. 실제로 안 쓰임(파라미터 맞추기용)
스텁(stub)물으면 정해진 답을 준다
스파이(spy)스텁에 더해 어떻게 불렸는지 기록한다
목(mock)불릴 것을 미리 기대하고, 그대로 됐는지 검증한다
페이크(fake)진짜처럼 동작하는 가벼운 구현(예: 메모리 DB)

이름이 많지만 핵심은 셋 - 스텁·목·페이크다. 나머지 둘은 그 변형으로 봐도 된다. 이 셋을 갈라 보자.

스텁: 정해진 답을 준다

스텁은 물으면 미리 정해둔 값을 돌려줄 뿐이다. “재고를 물으면 0을 반환하라”고 세팅해두는 식이다.

plaintext
stub.재고조회() → 항상 0을 반환

// 그 상태에서 주문하면 "재고 부족"으로 거부돼야 한다

스텁의 목적은 테스트 상황을 만드는 것이다. “재고 0일 때”를 진짜 DB 없이 연출한다. 그리고 결과(주문이 거부됐나)를 확인한다 - 이걸 상태 검증이라 한다. 무엇이 어떻게 됐는지를 본다.

목: 불렸는지를 확인한다

은 초점이 다르다. 반환값이 아니라 **“제대로 호출됐는가”**를 검증한다.

plaintext
mock: "메일 발송이 정확히 1번 불려야 한다"고 기대 설정

// 주문을 넣는다
// 검증: 발송이 1번 불렸나? 안 불렸으면 실패

메일 발송처럼 결과가 밖으로 나가서 반환값으로 확인할 수 없는 걸 잡는다 - “보냈는가”를 호출 여부로 검증한다. 이걸 행위 검증이라 한다.

diagramdiagram

스텁은 “무엇이 나왔나”, 목은 “무엇을 했나” - 이 차이가 둘을 가른다. 초중급이 가장 자주 헷갈리는 지점이다.

페이크: 진짜처럼 동작하는 가벼운 구현

페이크는 정해진 답이 아니라 진짜 로직을 가볍게 흉내 낸다. 대표가 메모리 저장소 - 진짜 DB 대신 Map에 넣고 빼지만, save하면 저장되고 find하면 나오는 실제 동작을 한다.

리포지토리 글의 InMemoryUserRepository가 바로 페이크다. 스텁이 “이 질문엔 이 답”을 하나씩 박아두는 것이라면, 페이크는 여러 호출에 일관되게 반응해서, 저장하고 다시 읽는 흐름까지 테스트할 수 있다.

과하게 끼우면 테스트가 거짓말한다

더블은 편하지만 과하면 독이다.

  • 전부 목으로 도배하면, 테스트가 “구현이 이 순서로 이 메서드들을 부른다”에 붙는다. 그러면 로직은 그대로인데 내부를 조금만 바꿔도 우수수 깨진다(깨지기 쉬운 테스트).
  • 더 위험한 건, 가짜끼리만 맞물려 초록불인데 진짜로는 안 도는 경우다. 가짜가 진짜와 다르게 굴면, 테스트는 통과해도 운영에서 터진다.

그래서 꼭 필요한 것만 갈아끼운다. 페이크로 될 걸 목으로 하지 말고, 진짜 맞물림은 통합 테스트에 맡긴다.

실무에서: 라이브러리와 판단

  • 목·스텁은 라이브러리로 만든다(Mockito 등). 손으로 안 짜도 한두 줄로 기대·반환을 세팅한다.
  • 상태로 될 건 상태로 - 반환값을 확인하면 되는데 굳이 “몇 번 불렸나”를 검증하면 구현에 묶인다. 목은 **부작용(발송·기록)**처럼 상태로 못 잡는 데만.
  • 더블은 경계에만. 내가 짠 로직끼리는 진짜로 두고, DB·외부 API·시간처럼 밖과 닿는 것만 가짜로.

정리

  • 진짜 의존성은 느리고 불안정하고 부작용이 있어서, 단위 테스트에선 가짜(테스트 더블)로 갈아끼운다. DI가 그걸 가능케 한다.
  • 핵심은 셋 - 스텁(정해진 답, 상태 검증), (호출 기대, 행위 검증), 페이크(진짜처럼 동작하는 가벼운 구현).
  • 스텁은 무엇이 나왔나, 목은 무엇을 했나 - 이 차이가 둘을 가른다.
  • 과한 목은 구현에 붙어 깨지기 쉽고, 가짜끼리만 맞으면 거짓 초록이 난다 - 꼭 필요한 경계에만.

다음 글은 그래서 무엇을 테스트하고 무엇은 안 하나, 테스트할 대상을 고르는 눈으로 간다.