내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자
자동차들이 "주어진 횟수만큼 이동 전략을 수행했는가?"에 대한 기능을 검증하려고 했다.
이는 자동차들이 매 회차마다 move()를 호출하는지를 확인하는 테스트로,
해당 로직은 자동차 경주 게임의 흐름을 제어하는 Game 클래스에서 수행된다.
따라서 테스트의 대상은 Game 클래스이며, 각 클래스의 역할은 다음과 같이 정리할 수 있다.
- RaceCount는 단순히 반복 횟수를 관리하고
- Cars는 자동차들을 한 번 움직이는 동작만 담당하며
- Game은 Game은 RaceCount와 Cars를 조합하여 경주를 진행한다.
즉, raceCount 횟수만큼 cars.move()를 호출하는 책임은 Game 클래스에게 있다.
✅ 검증할 코드
그런데 막상 검증할 코드를 볼 때는 Game.start() 내부에서 raceCount.play()가 호출되기도 하고, raceCount가 회차를 관리하는 클래스라 "raceCount.play()가 몇 번 호출되었는지를 검증해야 하는 것 아닌가"라는 생각이 들었다.
class Game
public List<RaceRoundResult> start() {
List<RaceRoundResult> responses = new ArrayList<>();
while (!raceCount.isDone()) {
List<Position> positions = raceCount.play(() -> cars.move());
responses.add(
new RaceRoundResult(
positions.stream()
.map(PositionResponse::new)
.collect(Collectors.toList())
)
);
}
return responses;
}
그래서 관련 의문을 gpt에게 물어봤고 아래와 같은 결론을 얻었다.
테스트는 외부에서 보이는 행동(기능)을 검증하는 것이 이상적입니다.
- cars.move()는 "자동차가 이동했다"는 결과가 바깥에 드러나는 행위
- raceCount.play()는 "게임 내부에서 횟수를 줄이기 위해 호출된 유틸 메서드" → 내부 구현에 가까움
정리하면, 테스트하려고 했던 기능은 "자동차들이 주어진 횟수만큼 이동 전략을 수행했냐"는 것이다. 그것에 대한 실질적인 메서드는 cars.move()이다. raceCount.play()는 Game.start()의 내부 사항일 뿐이다.
✅ 근거 자료
"내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자"는 관점을 답변으로 준 gpt에게 이런 내용은 어디서 기반한 것인지 물어봤다.
이런 내용은 "좋은 테스트란 무엇인가?", "테스트의 유지보수성과 신뢰성", "객체지향 설계에서 테스트를 어떻게 바라보는가?"와 관련돼 있어요.
그리고 책도 추천받았다.
📚 이런 내용을 다루는 대표적인 책들
1. 《테스트 주도 개발(Test-Driven Development: By Example) - Kent Beck》
- 테스트의 목적은 동작을 검증하는 것이라는 철학이 반복해서 나와요.
- 테스트는 외부에서 보이는 인터페이스에 집중해야 한다는 메시지가 명확히 나와요.
- 내부 구현 변경에도 테스트가 깨지지 않도록 작성하는 것이 중요하다고 강조합니다.
✅ 우리가 이야기한 "내부 구현 확인 테스트는 피하자"는 핵심 철학이 이 책의 중심이에요.
2. 《클린 코드(Clean Code) - Robert C. Martin》
- 테스트 코드는 "설계에 대한 두 번째 사용자" 라고 표현합니다.
- 구현 방식이 바뀌어도 테스트가 깨지지 않아야 한다는 주장을 펼쳐요.
- 특히, 구현 세부사항을 테스트하지 마라는 챕터가 있어요.
3. 《xUnit Test Patterns - Gerard Meszaros》
- 테스트 코드 자체의 패턴과 안티패턴에 대해 매우 상세하게 설명해요.
- 특히 다음 개념들이 중요하게 다뤄져요:
- Behavior Verification (행동 검증) vs State Verification (상태 검증)
- 테스트가 구현 세부사항에 너무 의존적이면 brittle test (잘 깨지는 테스트)가 됨
RaceCount.play() 호출 횟수를 확인하는 건 행동 검증처럼 보이지만,
실제로는 구현 방식 검증이라서 잘 깨지는 테스트가 될 수 있다는 예시로 여겨져요.
📖 관련 분야
- TDD (Test-Driven Development)
- 객체지향 설계와 테스트
- 유닛 테스트 원칙 (SOLID와 맞물림)
- 테스트 유지보수 전략
✅ 추가로 한 고민
그럼 굳이 Game.start()에서 RaceCount.play()를 호출하지 않고,
Cars.move()를 호출하면서 그 안에서 RaceCount.reduce()를 실행해도 되는 게 아닐까라는 생각이 들었다.