Test-Driven Development
Iron Law: FAILING TEST 없이는 어떠한 PRODUCTION 코드도 작성될 수 없습니다.
TDD가 협상 불가능한 이유
Test-Driven Development (TDD)는 소프트웨어 엔지니어링에서 가장 많이 논쟁되고 가장 자주 건너뛰는 관행입니다. 개발자들은 테스트를 먼저 작성하는 것이 느리다고, 특정 종류의 코드에는 어색하다고, 마감이 너무 가깝다고, 나중에 테스트를 추가하겠다고 주장합니다.
Superpowers는 이 모든 주장을 합리화로 취급하며 test-first 규율을 건너뛸 수 있는 이유로 허용하지 않습니다.
코드를 먼저 작성하는 것의 핵심 문제는 다음과 같습니다: 테스트 전에 코드를 작성하면, 테스트는 의도를 검증하는 것이 아니라 코드에 맞게 작성됩니다. 코드가 해야 하는 것이 아닌 코드가 하는 것을 테스트하게 됩니다. 테스트는 올바름 보장이 아닌 문서화 산물이 됩니다. 원래 코드에 존재했던 버그가 예상 동작으로 테스트에 인코딩됩니다.
테스트를 먼저 작성하면:
- 구현 방법을 결정하기 전에 원하는 정확한 동작을 명시하도록 강제됩니다
- 구현을 만들기 전에 인터페이스 문제를 발견합니다 (수정 비용이 저렴함)
- failing test는 테스트가 실제로 무언가를 테스트하고 있다는 증거입니다
- 구현 후 통과하는 테스트는 의도한 것을 만들었다는 증거입니다
TDD는 더 느리지 않습니다. 느린 부분을 제거하는 것입니다: 제대로 명세되지 않은 코드를 디버깅하는 데 소비된 시간.
RED-GREEN-REFACTOR 사이클
TDD는 모든 기능에 대해 간단한 3단계 사이클을 따릅니다:
┌─────────────────────────────────────────────────────────────┐
│ │
│ RED ──────────────────→ GREEN ──────────────→ REFACTOR │
│ │
│ Write a failing Make it pass with Improve the │
│ test for the the simplest code code without │
│ required behavior. possible. changing │
│ behavior. │
└─────────────────────────────────────────────────────────────┘
RED: Failing Test 작성
필요한 동작 하나를 정확히 설명하는 테스트를 작성하세요. 실행하세요. 실패하는 것을 보세요.
이 단계는 선택 사항이 아닙니다. 테스트를 작성했는데 구현 없이 즉시 통과한다면, 두 가지 중 하나가 사실입니다:
- 기능이 이미 존재합니다 (그리고 왜인지 확인해야 합니다)
- 테스트가 잘못되었습니다 — 생각한 것을 실제로 테스트하지 않습니다
구현 전에 통과하는 테스트는 가치가 없습니다. red 단계는 테스트가 진짜임을 확인하기 위해 존재합니다.
// RED: calculateDiscount가 아직 없기 때문에 이 테스트는 실패합니다
test('applies 10% discount for orders over $100', () => {
const order = { total: 150, items: [] };
expect(calculateDiscount(order)).toBe(15);
});
GREEN: 통과시키기 (단순하게)
failing test를 통과시키는 가장 단순한 코드를 작성하세요. 가장 우아한 코드가 아닙니다. 가장 확장 가능한 코드가 아닙니다. 가장 단순한 코드입니다.
이 규칙은 구현 단계에서의 과도한 엔지니어링을 방지합니다. 아직 테스트가 없는 케이스를 위해 만들 수 없습니다. "할인이 $500 이상 주문에 20%라면 어떻게 될까요?"라고 생각한다면 — 그것에 대한 테스트를 먼저 작성하고, 그다음에 통과시키세요.
// GREEN: 테스트를 통과시키는 가장 단순한 구현
function calculateDiscount(order: Order): number {
if (order.total > 100) {
return order.total * 0.10;
}
return 0;
}
REFACTOR: 손상 없이 개선
테스트가 통과하면 안전망이 생깁니다. 이제 코드를 개선할 수 있습니다: 더 나은 이름 짓기, 헬퍼 함수 추출, 중복 제거, 가독성 향상 — 동작을 변경하지 않고 품질을 향상시키는 모든 것.
모든 refactor 후에 테스트를 실행하세요. 여전히 통과한다면 아무것도 망가뜨리지 않은 것입니다. 테스트가 실패하면 마지막 변경을 되돌리세요.
// REFACTOR: 더 명확한 이름 짓기와 상수
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.10;
function calculateDiscount(order: Order): number {
const qualifiesForDiscount = order.total > DISCOUNT_THRESHOLD;
return qualifiesForDiscount ? order.total * DISCOUNT_RATE : 0;
}
테스트는 여전히 통과합니다. 코드는 더 깔끔합니다. 사이클이 완료되었습니다.
테스트 전에 코드가 작성된 경우 할 일
이 상황은 발생할 것입니다. AI 에이전트가 테스트 전에 구현 코드를 작성할 것입니다. 마감 압박 하의 개발자가 그렇게 할 것입니다. 문제는 그것이 발생했을 때 무엇을 해야 하는가입니다.
Superpowers는 명시적입니다:
삭제하세요. 처음부터 시작하세요.
"지금 테스트를 추가하세요"가 아닙니다. "그 정도면 충분합니다"도 아닙니다. 구현 코드를 삭제하고 먼저 테스트를 작성하세요.
이것은 이유를 이해할 때까지 가혹하게 보입니다. failing test 없이 작성된 코드에는 검증되지 않은 가정이 인코딩되어 있습니다. 사후에 테스트를 작성할 때, 통과할 것임을 아는 테스트를 작성하는 것입니다 — 즉 아무것도 발견하지 못합니다. 코드가 하는 것을 코드가 하는지 확인하는 것에 불과합니다.
test-first 규율은 단지 테스트를 갖는 것에 관한 것이 아닙니다. 구현 전에 동작을 명시하는 프로세스에 관한 것입니다. 사후 추가된 테스트는 이 단계를 건너뜁니다.
IF: Preceding failing test 없이 production 코드가 작성된 경우
THEN:
1. Production 코드를 삭제합니다
2. 삭제를 commit합니다
3. Failing test를 작성합니다
4. 테스트에 대해 구현합니다
5. 계속합니다
합리화 표
TDD를 건너뛰려는 압박이 쌓일 때, 다음은 일반적인 주장과 왜 그것이 통하지 않는지입니다:
| 변명 | 현실 |
|---|---|
| "나중에 테스트를 추가할게요" | 나중에 추가된 테스트는 코드가 해야 하는 것이 아닌 코드가 하는 것을 테스트합니다. 나중은 거의 오지 않습니다. |
| "이 코드는 너무 간단해서 테스트가 필요 없어요" | 간단한 코드에도 버그가 있습니다. 테스트는 2분 걸립니다. |
| "마감이 내일이에요" | 빠르게 납품된 테스트되지 않은 코드는 테스트를 작성하는 것보다 더 오래 걸리는 버그를 만듭니다. |
| "이런 종류의 코드에 대한 테스트를 작성할 수 없어요" | 할 수 있습니다. 테스트하기 정말 어렵다면, 코드 설계가 잘못되었다는 신호입니다. |
| "먼저 탐색적 코딩을 하고 나서 형식화할게요" | 탐색적 코드가 production 코드가 됩니다. 삭제하거나 테스트하거나 — 세 번째 선택지는 없습니다. |
| "AI가 생성했으니 아마 괜찮을 거예요" | AI가 생성한 코드에도 버그가 있습니다. 테스트 없는 AI 생성 코드에는 미탐지 버그가 있습니다. |
| "지금 테스트를 추가하면 속도가 느려질 거예요" | production에서 버그를 발견한 후 테스트를 추가하는 것이 훨씬 더 느립니다. |
| "이것을 테스트하려면 너무 많은 것을 mocking해야 해요" | 너무 많은 mock은 설계에 너무 많은 종속성이 있다는 의미입니다. 설계를 수정하세요. |
완료 전 Verification
기능이 완료됐다고 주장하기 전에, Superpowers verification 프로토콜은 5단계 gate를 요구합니다:
┌────────────────────────────────────────────────────────────────┐
│ VERIFICATION GATE │
│ │
│ Step 1: IDENTIFY — List every test relevant to this feature │
│ Step 2: RUN — Execute the test suite (don't just assume) │
│ Step 3: READ — Read the actual output, line by line │
│ Step 4: VERIFY — Confirm each test passes in the output │
│ Step 5: CLAIM — Only now state that the feature is complete │
└────────────────────────────────────────────────────────────────┘
Step 1: IDENTIFY
기능을 다루는 모든 테스트 파일과 테스트 케이스를 나열하세요. 오늘 작성한 테스트뿐만 아니라 — 영향받는 코드를 건드리는 모든 기존 테스트.
Step 2: RUN
실제로 테스트 스위트를 실행하세요. 머릿속에서가 아닙니다. 가정하지 마세요. 실행하세요.
npm test -- --coverage --testPathPattern=order
Step 3: READ
실제 출력을 읽으세요. 모든 줄. "전반적으로 통과했는지 실패했는지"만이 아닙니다. 다음을 보세요:
- 어떤 테스트가 실행되었는지
- 어떤 테스트가 통과했는지
- 어떤 테스트가 스킵되었는지
- 커버리지 수치
Step 4: VERIFY
IDENTIFY 목록의 각 테스트에 대해, 그것이 출력에 나타나고 통과로 표시되는지 확인하세요. 테스트가 출력에서 누락되었다면 실행되지 않은 것입니다. 테스트가 실패하고 있다면 기능은 완료되지 않은 것입니다.
Step 5: CLAIM
1~4단계를 완료한 후에만 AI(또는 개발자)가 "이 기능은 완료됐습니다"라고 말할 수 있습니다.
"작동해야 합니다"라고 말하는 것은 5단계를 완료한 것이 아닙니다. 실제 테스트 출력만이 5단계를 완료합니다.
즉시 중단이 필요한 Red Flags
TDD 중 다음 상황이 발생하면, 즉시 작업을 중단하고 에스컬레이션하세요:
| Red Flag | 의미 |
|---|---|
| 구현 존재 전에 테스트가 통과함 | 테스트가 아마도 잘못되었음 |
| 테스트는 통과하지만 기능이 올바르게 동작하지 않음 | 테스트가 잘못된 것을 테스트하고 있음 |
| 대규모 refactor 후 첫 실행에서 스위트의 모든 테스트가 통과함 | 의심스러움 — 테스트가 우연히 스킵되지 않았는지 확인 |
| 새 기능을 추가할 때 테스트 커버리지가 감소함 | 갭이 도입되고 있음 |
| 하나의 테스트를 통과시키기 위해 다른 15개 테스트를 변경해야 함 | 설계 변경이 너무 컸음; 더 작은 단계로 나누어야 함 |
| AI가 출력을 보여주지 않고 테스트가 통과했다고 주장함 | 허용 불가 — 계속 진행하기 전에 실제 출력을 요구하세요 |
Superpowers 계획 맥락에서의 TDD
Writing Plans에서 언급된 것처럼, 계획의 모든 구현 작업은 테스트 작업이 선행되어야 합니다. 계획 구조는 계획 수준에서 TDD를 강제합니다:
Task N: Write failing test for [behavior] ← RED
Task N+1: Implement [behavior] ← GREEN
[Refactor happens within Task N+1 or as Task N+2 if scope warrants]
계획을 실행할 때, Task N (테스트)이 먼저 완료되지 않고 Task N+1 (구현)을 위한 subagent가 디스패치된다면, subagent는 작업을 거부하고 BLOCKED로 보고해야 합니다. 선행 failing test 없는 구현 작업은 계획 결함입니다.
Superpowers로 TDD 실행하기
TDD skill을 명시적으로 호출하려면:
/test-driven-development I need to implement user authentication
AI는 RED-GREEN-REFACTOR 사이클을 안내하고, failing-test-first 요구사항을 강제하며, 완료로 표시하기 전에 5단계 verification gate를 적용할 것입니다.
TDD는 코드가 작동한다고 믿는 것과 작동한다는 것을 아는 것의 차이입니다. 전문적인 소프트웨어 개발에서 믿음은 허용 가능한 기준이 아닙니다. 증거가 기준입니다.