목차
단위 테스트가 빠른 건 조각 하나만 보기 때문이다. 그런데 그 조각에 진짜 의존성이 딸려 오면 빠름이 무너진다. 그걸 떼는 도구가 테스트 더블이다.
진짜 의존성이 걸림돌이다
주문 서비스의 “재고가 모자라면 거부한다”만 확인하고 싶다. 그런데 이 서비스는 재고 DB를 부르고, 결제 API를 호출하고, 알림 메일을 보낸다.
주문 서비스 → 재고 DB (띄워야 함)
→ 결제 API (진짜 결제되면 곤란)
→ 메일 발송 (테스트마다 메일이?)로직 하나 보려는데 진짜를 다 붙이면 느리고, 불안정하고(외부가 죽으면 테스트도 실패), 부작용까지 난다(진짜 결제·진짜 메일). 확인하려는 건 재고 규칙 하나뿐인데.
그래서 가짜로 갈아끼운다
영화의 스턴트 대역처럼, 진짜 의존성 자리에 가짜를 끼운다. 이 가짜들을 통틀어 테스트 더블(test double)이라 한다.
갈아끼우는 게 가능한 건 의존성 역전 덕이다. 서비스가 구체 클래스가 아니라 인터페이스에 기대고 있으면, 운영에선 진짜를, 테스트에선 가짜를 주입하면 된다. 리포지토리 패턴 글이 바로 이 교체를 구체 코드로 보여준다.
더블에도 종류가 있다
“가짜”라고 다 같지 않다. 무엇을 하느냐에 따라 다섯으로 나눈다(Gerard Meszaros의 분류).
| 종류 | 하는 일 |
|---|---|
| 더미(dummy) | 자리만 채운다. 실제로 안 쓰임(파라미터 맞추기용) |
| 스텁(stub) | 물으면 정해진 답을 준다 |
| 스파이(spy) | 스텁에 더해 어떻게 불렸는지 기록한다 |
| 목(mock) | 불릴 것을 미리 기대하고, 그대로 됐는지 검증한다 |
| 페이크(fake) | 진짜처럼 동작하는 가벼운 구현(예: 메모리 DB) |
이름이 많지만 핵심은 셋 - 스텁·목·페이크다. 나머지 둘은 그 변형으로 봐도 된다. 이 셋을 갈라 보자.
스텁: 정해진 답을 준다
스텁은 물으면 미리 정해둔 값을 돌려줄 뿐이다. “재고를 물으면 0을 반환하라”고 세팅해두는 식이다.
stub.재고조회() → 항상 0을 반환
// 그 상태에서 주문하면 "재고 부족"으로 거부돼야 한다스텁의 목적은 테스트 상황을 만드는 것이다. “재고 0일 때”를 진짜 DB 없이 연출한다. 그리고 결과(주문이 거부됐나)를 확인한다 - 이걸 상태 검증이라 한다. 무엇이 어떻게 됐는지를 본다.
목: 불렸는지를 확인한다
목은 초점이 다르다. 반환값이 아니라 **“제대로 호출됐는가”**를 검증한다.
mock: "메일 발송이 정확히 1번 불려야 한다"고 기대 설정
// 주문을 넣는다
// 검증: 발송이 1번 불렸나? 안 불렸으면 실패메일 발송처럼 결과가 밖으로 나가서 반환값으로 확인할 수 없는 걸 잡는다 - “보냈는가”를 호출 여부로 검증한다. 이걸 행위 검증이라 한다.
스텁은 “무엇이 나왔나”, 목은 “무엇을 했나” - 이 차이가 둘을 가른다. 초중급이 가장 자주 헷갈리는 지점이다.
페이크: 진짜처럼 동작하는 가벼운 구현
페이크는 정해진 답이 아니라 진짜 로직을 가볍게 흉내 낸다. 대표가 메모리 저장소 - 진짜 DB 대신 Map에 넣고 빼지만, save하면 저장되고 find하면 나오는 실제 동작을 한다.
리포지토리 글의 InMemoryUserRepository가 바로 페이크다. 스텁이 “이 질문엔 이 답”을 하나씩 박아두는 것이라면, 페이크는 여러 호출에 일관되게 반응해서, 저장하고 다시 읽는 흐름까지 테스트할 수 있다.
과하게 끼우면 테스트가 거짓말한다
더블은 편하지만 과하면 독이다.
- 전부 목으로 도배하면, 테스트가 “구현이 이 순서로 이 메서드들을 부른다”에 붙는다. 그러면 로직은 그대로인데 내부를 조금만 바꿔도 우수수 깨진다(깨지기 쉬운 테스트).
- 더 위험한 건, 가짜끼리만 맞물려 초록불인데 진짜로는 안 도는 경우다. 가짜가 진짜와 다르게 굴면, 테스트는 통과해도 운영에서 터진다.
그래서 꼭 필요한 것만 갈아끼운다. 페이크로 될 걸 목으로 하지 말고, 진짜 맞물림은 통합 테스트에 맡긴다.
실무에서: 라이브러리와 판단
- 목·스텁은 라이브러리로 만든다(Mockito 등). 손으로 안 짜도 한두 줄로 기대·반환을 세팅한다.
- 상태로 될 건 상태로 - 반환값을 확인하면 되는데 굳이 “몇 번 불렸나”를 검증하면 구현에 묶인다. 목은 **부작용(발송·기록)**처럼 상태로 못 잡는 데만.
- 더블은 경계에만. 내가 짠 로직끼리는 진짜로 두고, DB·외부 API·시간처럼 밖과 닿는 것만 가짜로.
정리
- 진짜 의존성은 느리고 불안정하고 부작용이 있어서, 단위 테스트에선 가짜(테스트 더블)로 갈아끼운다. DI가 그걸 가능케 한다.
- 핵심은 셋 - 스텁(정해진 답, 상태 검증), 목(호출 기대, 행위 검증), 페이크(진짜처럼 동작하는 가벼운 구현).
- 스텁은 무엇이 나왔나, 목은 무엇을 했나 - 이 차이가 둘을 가른다.
- 과한 목은 구현에 붙어 깨지기 쉽고, 가짜끼리만 맞으면 거짓 초록이 난다 - 꼭 필요한 경계에만.
다음 글은 그래서 무엇을 테스트하고 무엇은 안 하나, 테스트할 대상을 고르는 눈으로 간다.