목, 스텁, 그리고 테스트 스타일
- 5.1 목과 스텁 구분하기
- 5.2 식별할 수 있는 동작과 구현 세부사항
- 5.3 목과 테스트 취약성
- 6.1 단위 테스트의 세 가지 스타일
- 6.2 함수형 아키텍처로 향하기
목을 많이 쓸수록 테스트가 좋아진다는 생각이 있어. 근데 실제로는 반대야. 목이 많아질수록 테스트가 취약해지거든. 그 이유를 이해하면 자연스럽게 어떤 테스트 스타일이 좋은지도 보여.
목과 스텁은 헷갈리기 쉬운데 역할이 달라. **목(Mock)**은 SUT가 의존성에 올바른 호출을 하는지 검증하는 거야. **스텁(Stub)**은 미리 정해진 값을 반환하는 거야. 목은 행위 검증, 스텁은 입력값 제공이야. 둘을 섞어서 쓸 수 있지만 개념을 구분해야 해. 스텁으로 검증까지 하려 하면 테스트가 읽기 어려워지거든.
그럼 무엇을 검증해야 하냐면, **식별할 수 있는 동작(observable behavior)**이야. 외부에서 봤을 때 결과가 맞으면 돼. 내부 메서드 이름, 호출 순서 같은 구현 세부사항을 테스트하면 리팩토링할 때마다 테스트가 깨져. 목이 과도하게 많은 테스트가 취약한 이유가 바로 여기 있어. 목은 SUT가 의존성과 어떻게 상호작용하는지를 검증하거든. 이건 구현 세부사항이야. 기능은 그대로인데 내부 로직을 리팩토링했을 뿐인데 테스트가 실패하는 상황 — 거짓 양성이야. 목은 시스템 경계에서만, 즉 외부 공유 의존성에만 써야 해.
이 맥락에서 테스트 스타일을 세 가지로 나눌 수 있어. **출력 기반(Output-based)**은 함수에 입력을 주고 반환값만 검증해. 내부를 전혀 신경 쓰지 않으니까 리팩토링 내성이 제일 높아. **상태 기반(State-based)**은 행동 후 객체의 상태를 확인해. 합리적인 수준이야. **통신 기반(Communication-based)**은 목을 써서 특정 호출을 검증해. 내부 호출 패턴이 구현 세부사항이기 때문에 리팩토링 내성이 가장 낮아. 4장 기준으로 줄 세우면 출력 기반이 최고, 통신 기반이 꼴찌야.
출력 기반 테스트를 많이 쓰려면 코드가 순수 함수로 짜여 있어야 해. 그래서 함수형 아키텍처가 나와. 사이드 이펙트를 가진 코드를 바깥쪽 껍데기로 밀어내고, 안쪽 코어는 순수 함수로 만드는 거야. 파일 쓰기, DB 쓰기는 경계 밖으로 보내고, 도메인 로직은 순수하게 유지해. 모든 코드를 함수형으로 바꾸기는 어렵지만, 도메인 로직만큼은 순수 함수로 만들면 출력 기반 테스트의 혜택을 크게 볼 수 있어.
정리
4장 읽고 기억할 거 세 가지:
- 목 ≠ 스텁: 목은 호출 검증, 스텁은 입력 제공. 목을 과도하게 쓰면 구현 세부사항에 결합되어 리팩토링마다 테스트가 깨져
- 출력 기반 테스트가 최고: 입력-출력만 보는 테스트가 리팩토링 내성이 가장 높아. 통신 기반(목 범벅)이 꼴찌
- 도메인 로직을 순수 함수로: 사이드 이펙트를 경계 밖으로 밀어내면 출력 기반 테스트를 더 많이 쓸 수 있어