Test
1. 테스트 지침
- 이 문서는 LLM이 테스트 코드를 작성/리뷰할 때 참고할 지침서다.
- Vladimir Khorikov의 『단위 테스트』(원제: Unit Testing: Principles, Practices, and Patterns)를 바탕으로,
docs/Test/Unit-Testing에 정리된 내용을 LLM이 코드를 생성할 때 바로 적용할 수 있는 규칙 형태로 재구성했다. - 이 지침의 핵심 결론 하나만 기억한다면: 테스트는 구현 방법이 아니라 최종 결과(식별할 수 있는 동작)를 검증해야 한다.
2. 테스트를 작성하는 이유
- 테스트의 목표는 커버리지 수치가 아니라 프로젝트가 장기적으로 지속 가능한 속도로 성장하도록 돕는 것이다.
- 코드 커버리지는 참고 지표일 뿐 품질을 보장하지 않는다. 100% 커버리지를 채우려고 무가치한 테스트를 양산하지 않는다.
- 시스템의 가장 중요한 부분(비즈니스 로직, 도메인 모델)에 테스트를 집중하고, 단순한 보일러플레이트 코드(단순 getter/setter, 프레임워크 설정)에는 테스트를 강요하지 않는다.
- 가치가 유지비를 넘어서는 테스트만 남긴다. 실행되지 않거나 매번 깨지기만 하는 테스트는 안전망이 아니라 짐이다.
3. 좋은 테스트를 가르는 4가지 기준
새로 작성하거나 리뷰하는 테스트는 아래 4가지 기준으로 평가한다.
| 기준 | 의미 |
|---|---|
| 회귀 방지 | 실제로 버그가 있을 때 테스트가 이를 잡아내는가 |
| 리팩터링 내성 | 동작을 바꾸지 않는 리팩터링에도 테스트가 통과하는가 (거짓 양성이 없는가) |
| 빠른 피드백 | 테스트가 얼마나 빨리 실행되는가 |
| 유지 보수성 | 테스트를 이해하고 실행하기 얼마나 쉬운가 |
- 이 네 가지는 서로 상충한다. 특히 회귀 방지·리팩터링 내성·빠른 피드백은 하나를 100% 얻으려면 다른 하나를 희생해야 하는 관계다.
- 네 가지 중 리팩터링 내성만은 절대 희생하지 않는다. 리팩터링 내성은 0 아니면 1이지 중간이 없기 때문이다. 리팩터링할 때마다 애먼 테스트가 깨진다면, 그 테스트는 실질적인 안전망이 아니라 유지비만 발생시키는 애물단지다.
- 리팩터링 내성이 없는 테스트(거짓 양성)가 반복되면 개발자는 실패를 무시하는 습관이 생기 고, 결국 진짜 회귀도 못 잡아낸다.
// 거짓 양성을 유발하는 예: 구현 세부 사항(내부에서 사용하는 하위 컴포넌트 목록)에 결합됨
@Test
fun `MessageRenderer는 올바른 서브 렌더러를 사용한다`() {
val sut = MessageRenderer()
val renderers = sut.subRenderers // 내부 구현을 노출/검증
assertThat(renderers).hasSize(3)
assertThat(renderers[0]).isInstanceOf(HeaderRenderer::class.java)
}
// 개선: 최종 결과(렌더링된 HTML)만 검증 -> 내부 구현이 바뀌어도 테스트는 안 깨진다
@Test
fun `메시지를 렌더링하면 header body footer가 합쳐진 HTML을 반환한다`() {
val message = Message(header = "h", body = "b", footer = "f")
val sut = MessageRenderer()
val html = sut.render(message)
assertThat(html).isEqualTo("<header>h</header><b>b</b><footer>f</footer>")
}