
실패하는 테스트가 있을 때에만 프로덕션 코드를 작성할 수 있다.
아직 실패한 테스트가 없다면, 코드를 작성해서는 안 된다.
테스트가 실패하지 않는다는 것은, 새로운 동작이 정의되지 않았다는 뜻이다.
실패를 확인할 수 있을 만큼만 테스트를 작성해야 한다.
한 번에 모든 테스트를 작성하지 않는다.
최소한의 실패를 만들어내는 테스트 한 줄이면 충분하다.
이 실패가 개발의 방향을 결정한다.
실패한 테스트를 통과하기 위한 최소한의 코드만 작성해야 한다.
완벽한 코드를 만들려 하지 말고,
“테스트를 통과시키는 데 필요한 최소한의 코드” 만 작성한다.
이는 과도한 설계와 불필요한 복잡도를 방지한다.
TDD의 핵심은 단 하나의 반복 주기다.
Red → Green → Refactor
먼저 실패하는 테스트를 작성한다.
이 단계에서의 목표는 구현이 아니라, “해야 할 일”을 명확히 정의하는 것이다.
@Test
void should_return_sum_of_two_numbers() {
assertEquals(5, Calculator.add(2, 3)); // 아직 Calculator가 없음 → 실패
}
테스트가 실패함으로써, 아직 구현되지 않은 요구사항이 드러난다.
이것이 바로 설계의 출발점이다.
테스트를 통과할 만큼의 최소한의 코드만 작성한다.
public class Calculator {
public static int add(int a, int b) {
return a + b; // 테스트 통과를 위한 최소 구현
}
}
이 시점에서는 코드가 어설퍼도 괜찮다.
중요한 것은 빨간색(Red)을 초록색(Green)으로 바꾸는 것이다.
모든 테스트가 통과했다면, 코드를 정리한다.
중복을 제거하고, 의도를 명확히 한다.
테스트가 있으므로 리팩토링은 두렵지 않다.
public class Calculator {
public static int add(int... numbers) {
return Arrays.stream(numbers).sum(); // 구조 개선
}
}
테스트를 다시 실행했을 때 모두 초록색이면,
하나의 TDD 사이클이 끝난다.
가장 단순한 테스트부터 시작하라.
복잡한 기능부터 테스트하려 하면 설계가 흐트러진다.
단순한 사례를 통해 코드를 단계적으로 구체화해야 한다.
동작하도록 만들기 위한 최소한의 코드만 작성하라.
불필요한 코드나 미리 짜둔 설계는 오히려 발목을 잡는다.
코드는 테스트를 통과하기 위한 정도로만 작성한다.
테스트가 구체화될수록, 프로덕션 코드는 범용적으로 성장한다.
테스트는 특정 상황을 검증하지만,
그 과정에서 프로덕션 코드는 점점 더 일반화된다.
이는 코드의 재사용성과 유연성을 높인다.
디버깅 시간이 줄어든다.
테스트가 즉각적인 피드백을 제공하므로,
디버깅에 소비되는 시간을 최소화할 수 있다.
테스트는 살아 있는 설계 문서가 된다.
테스트 코드는 시스템이 “무엇을 해야 하는지”를 명세하는 문서다.
코드가 변경되면 테스트가 깨지므로, 항상 최신 문서로 유지된다.
더 나은 설계가 자연스럽게 도출된다.
테스트를 쉽게 만들기 위해
자연스럽게 의존성 분리, 단일 책임 원칙(SRP), 캡슐화 같은 좋은 설계가 따라온다.
안전한 리팩토링이 가능하다.
테스트가 방어막 역할을 하므로,
코드를 마음껏 수정해도 기존 동작이 보장된다.
| 구분 | TDD | TAD |
|---|---|---|
| 테스트 시점 | 구현 이전 | 구현 이후 |
| 초점 | 설계 주도 (Design-Driven) | 동작 검증 (Validation) |
| 커버리지 | 전체 흐름 | 일부 기능 |
| 신뢰도 | 높음 | 낮음 |
TAD는 “되는 기능만 테스트할 가능성”이 높다.
즉, 실패를 경험하지 않은 테스트는 신뢰할 수 없다.
| 항목 | 핵심 내용 |
|---|---|
| Three Laws | 실패 테스트만 작성 → 최소한의 테스트 → 최소한의 코드 |
| Cycle | Red → Green → Refactor |
| Principles | 쉬운 테스트부터, 최소 코드, 테스트가 설계를 구체화 |
| Benefits | 디버깅 감소, 자동 문서화, 좋은 설계, 안전한 리팩토링 |