클린 코드 실전 강의 교재¶
9장 — 단위 테스트¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → Before/After → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, JUnit 5, Spring Boot 3.x
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- TDD 3법칙 — Beck의 사이클을 손에 익힘.
- F.I.R.S.T. — 좋은 테스트의 5속성.
- 테스트는 프로덕션 코드만큼 깨끗 해야 한다.
- 테스트 당 assert 하나 / 개념 하나.
0.2 큰 그림¶
[ TDD 3법칙 ] [ 깨끗한 테스트 ] [ F.I.R.S.T. ]
실패 테스트 먼저 도메인 특화 언어 (DSL) Fast
통과 최소 코드 이중 표준 (테스트는 가독성) Independent
정련 후 다음 Repeatable
Self-validating
Timely
비유 — 단위 테스트는 "코드의 헬스장"입니다.
매일 짧게 운동해야 근육 유지. 한 번에 5시간 운동 = 다친다. 빠르고·자주·일관되게 — 그래야 진짜 효과.
0.3 현업에서 왜 중요한가¶
- 리팩터링 4장 "테스트가 모든 리팩터링의 전제" 와 직결.
- Effective Java Item 49 매개변수 검증, Item 56 Javadoc 같은 권고는 테스트로 검증 될 때 비로소 의미.
- 더러운 테스트 = 차라리 없는 게 나음 (유지보수 비용·신뢰 깨짐).
9.1 TDD 법칙 세 가지¶
Kent Beck의 3법칙¶
- 실패하는 단위 테스트 를 작성할 때까지 프로덕션 코드 작성 금지.
- 컴파일은 되지만 실패 하는 정도로만 테스트 작성.
- 현재 실패 테스트를 통과시킬 최소한의 코드 만 작성.
결과¶
- 매우 짧은 사이클 (몇 분).
- 테스트 코드와 프로덕션 코드가 함께 자란다.
- 테스트 안 한 코드는 없음 (100% 커버는 자연스러운 부산물).
Spring TDD 사이클¶
1. Red: 테스트 작성 — 컴파일 실패
2. 인터페이스/클래스 골조 만들기 — 컴파일 통과, 테스트 실패 (Red)
3. Green: 통과시킬 최소 구현
4. Refactor: 중복 제거·이름 정리 (테스트가 안전망)
5. 다음 테스트로
9.2 깨끗한 테스트 코드 유지하기¶
한 줄 정의¶
테스트 코드는 프로덕션 코드만큼 깨끗 해야 한다. 더러운 테스트 = 없는 테스트보다 나쁨.
더러운 테스트의 비용¶
- 코드 변경 → 테스트 변경 부담 폭증
- 변경이 두려워 코드 정리 못 함 → 코드 부패
- 결과: 테스트 없는 프로젝트로 회귀
9.2.1 테스트는 유연성·유지보수성·재사용성을 제공한다¶
테스트가 있으면 → 변경이 두렵지 않음 → 깨끗하게 유지 → 진화 가능.
테스트가 없으면 → 변경 무서움 → 부패 → 폐기.
9.3 깨끗한 테스트 코드¶
핵심 원칙 — 가독성¶
좋은 테스트의 핵심은 가독성. 그것도 프로덕션 코드보다 더.
9.3.1 도메인 특화 테스트 언어 (DSL)¶
테스트가 도메인의 단어로 표현되게 헬퍼 함수를 작성.
// Before
@Test void test() {
repository.save(new Order(userId, List.of(item1, item2), DiscountPolicy.NONE));
Order found = repository.findById(order.id()).orElseThrow();
assertThat(found.total()).isEqualTo(Money.won(15000));
}
// After — 도메인 DSL
@Test void test() {
Order order = anOrder().withUser("u1").withItems(item1, item2).build();
repository.save(order);
assertThat(repository.findById(order.id()).orElseThrow().total())
.isEqualTo(Money.won(15000));
}
9.3.2 이중 표준 (Dual Standard)¶
테스트는 프로덕션과 다른 기준 도 OK: - 효율보다 가독성 우선 - 메모리·CPU 비용 약간 더 허용 - 단, 구조·이름·중복 제거 는 동일
9.4 테스트 당 assert 하나¶
한 줄 정의¶
각 테스트는 한 가지 사실 만 검증. assert가 여러 개 필요하면 보통 테스트가 여러 개.
Before / After¶
// Before — 한 테스트에 여러 검증
@Test void 주문() {
Order o = repository.save(order);
assertThat(o.id()).isNotNull();
assertThat(o.status()).isEqualTo(DRAFT);
assertThat(o.items()).hasSize(2);
assertThat(o.total()).isEqualTo(Money.won(15000));
// 첫 번째 실패 → 나머지 못 봄
}
// After — 한 시나리오 한 검증
@Test void 저장된_주문은_ID가_부여된다() { ... }
@Test void 새_주문의_상태는_DRAFT() { ... }
@Test void 주문은_품목_수를_안다() { ... }
@Test void 주문은_합계를_계산한다() { ... }
9.4.1 테스트 당 개념 하나¶
엄격히 "assert 1개" 가 아니라 개념 1개. 한 가지 동작을 검증하는 데 assert 2~3개가 자연스럽다면 OK.
@Test void 주문_생성_후_초기_상태() {
Order o = newOrder();
assertThat(o.status()).isEqualTo(DRAFT); // 개념: "초기 상태"
assertThat(o.items()).isEmpty(); // 같은 개념의 다른 면
}
9.5 F.I.R.S.T.¶
좋은 테스트의 5속성.
F — Fast (빠르게)¶
- 단위 테스트는 ms 단위
- DB·HTTP·파일 안 씀
- 느린 테스트는 안 돌게 됨 → 의미 상실
I — Independent (독립적)¶
- 테스트끼리 순서 의존 X
- 한 테스트 실패가 다른 테스트 실패를 가리지 않음
R — Repeatable (반복 가능)¶
- 네트워크·시간·랜덤에 의존 X
- 같은 환경에서 항상 같은 결과
S — Self-Validating (자가 검증)¶
- 출력 보고 사람이 판단 X
- assert 로 자동 통과/실패
T — Timely (적시에)¶
- 프로덕션 코드 직전 작성 (TDD)
- 프로덕션 먼저 → 테스트가 어려워지는 구조 굳어짐
핵심 교훈¶
- TDD 3법칙 — 짧은 빨강-초록-리팩터 사이클.
- 테스트는 프로덕션만큼 깨끗 해야 — 더러운 테스트 = 없는 게 나음.
- 도메인 DSL 로 테스트 가독성 폭증.
- 이중 표준 — 효율 < 가독성 (테스트만).
- 테스트 당 개념 하나 — assert 1개는 가이드, 본질은 한 시나리오.
- F.I.R.S.T. — 빠르고·독립·반복·자가검증·적시에.
함정 / 주의¶
- flaky 테스트 (가끔 실패) 는 즉시 원인 추적·고정·삭제 중 하나. 방치하면 신뢰 깨짐.
- 통합 테스트는 단위 테스트와 별도 카테고리 — F.I.R.S.T. 의 F (빠름) 는 단위에 한정.
- 100% 커버리지는 목적 아님. 핵심 비즈니스·과거 사고 부위에 집중.
체크리스트 (팀 규율로)¶
- CI에서 단위 테스트 1분 안에 도는가
- 테스트가 다른 테스트 결과에 의존하지 않는가
- DB·시간·랜덤에 의존하는 테스트가 없는가 (단위 영역에서)
- flaky 테스트가 있는가 → 즉시 처리
- 테스트 이름이 동작을 한국어로 말하는가
- 한 테스트 = 한 개념인가
- 도메인 DSL 헬퍼가 있는가 (반복 셋업 정리)
- 프로덕션 변경 시 테스트 변경 비용이 작은가 (구현 디테일 검증 회피)
퀴즈¶
Q1. Beck의 TDD 3법칙을 한 문장으로 요약하면?
A. 실패하는 작은 테스트를 먼저, 통과시킬 최소 코드만, 그 다음 정련. 매우 짧은 사이클로 테스트와 프로덕션이 함께 자란다.
Q2. "더러운 테스트가 없는 테스트보다 나쁘다" 의 의미?
A. 더러우면 (1) 코드 변경 시 테스트 변경 부담 폭증, (2) 변경 두려움 → 코드 부패, (3) 결국 테스트 폐기. 차라리 처음부터 없는 게 부패 속도가 늦음.
Q3. F.I.R.S.T. 의 "Independent" 가 깨지면 무엇이 어려워지는가?
A. 테스트 순서·격리 가 깨져 한 테스트 실패가 다른 테스트 실패를 일으킴. CI에서 진짜 원인 추적 어려움. 병렬 실행 불가. 신규 테스트 추가가 기존 테스트 깨뜨림.
Q4. "테스트 당 assert 하나" 가 엄격한 규칙이 아니라는 의미?
A. 본질은 한 테스트 = 한 개념. 한 동작 검증에 자연스러운 assert 2~3개는 OK (예: "초기 상태"는 status + items 검증). 다른 개념이 섞이면 분리.
Q5. 도메인 DSL 헬퍼가 테스트 가독성에 주는 효과?
A. 테스트 본문이 도메인 단어 로 표현됨 (anOrder().withUser(...)). 셋업 잡음 제거 → 시나리오·검증이 한눈에. 헬퍼 자체도 재사용 가능.
다음 장 예고 — 10장: 클래스¶
SRP·응집도·작은 클래스 여럿 — 클래스 단위의 규율. 작은 함수가 모이면 자연스럽게 작은 클래스가 나옴. 변경하기 쉬운 클래스의 비법.