콘텐츠로 이동

클린 코드 실전 강의 교재

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법칙

  1. 실패하는 단위 테스트 를 작성할 때까지 프로덕션 코드 작성 금지.
  2. 컴파일은 되지만 실패 하는 정도로만 테스트 작성.
  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)
  • 프로덕션 먼저 → 테스트가 어려워지는 구조 굳어짐

핵심 교훈

  1. TDD 3법칙 — 짧은 빨강-초록-리팩터 사이클.
  2. 테스트는 프로덕션만큼 깨끗 해야 — 더러운 테스트 = 없는 게 나음.
  3. 도메인 DSL 로 테스트 가독성 폭증.
  4. 이중 표준 — 효율 < 가독성 (테스트만).
  5. 테스트 당 개념 하나 — assert 1개는 가이드, 본질은 한 시나리오.
  6. 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·응집도·작은 클래스 여럿 — 클래스 단위의 규율. 작은 함수가 모이면 자연스럽게 작은 클래스가 나옴. 변경하기 쉬운 클래스의 비법.