test-driven-development · diff
git:20260501.402017b to git:20260501.3acfa20
101 added, 340 removed. Audit A to A.
---
name: test-driven-development
description: Use when implementing any feature or bugfix, before writing implementation code
---
- # Test-Driven Development (TDD)
-
- ## Overview
-
- Write the test first. Watch it fail. Write minimal code to pass.
-
- **Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
-
- **Violating the letter of the rules is violating the spirit of the rules.**
-
- ## When to Use
-
- **Always:**
- - New features
- - Bug fixes
- - Refactoring
- - Behavior changes
-
- **Exceptions (ask your human partner):**
- - Throwaway prototypes
- - Generated code
- - Configuration files
-
- Thinking "skip TDD just this once"? Stop. That's rationalization.
-
- ## The Iron Law
-
- ```
- NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
- ```
-
- Write code before the test? Delete it. Start over.
-
- **No exceptions:**
- - Don't keep it as "reference"
- - Don't "adapt" it while writing tests
- - Don't look at it
- - Delete means delete
-
- Implement fresh from tests. Period.
-
- ## Red-Green-Refactor
-
- ```dot
- digraph tdd_cycle {
- rankdir=LR;
- red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
- verify_red [label="Verify fails\ncorrectly", shape=diamond];
- green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
- verify_green [label="Verify passes\nAll green", shape=diamond];
- refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
- next [label="Next", shape=ellipse];
-
- red -> verify_red;
- verify_red -> green [label="yes"];
- verify_red -> red [label="wrong\nfailure"];
- green -> verify_green;
- verify_green -> refactor [label="yes"];
- verify_green -> green [label="no"];
- refactor -> verify_green [label="stay\ngreen"];
- verify_green -> next;
- next -> red;
- }
- ```
-
- ### RED - Write Failing Test
-
- Write one minimal test showing what should happen.
-
- <Good>
- ```typescript
- test('retries failed operations 3 times', async () => {
- let attempts = 0;
- const operation = () => {
- attempts++;
- if (attempts < 3) throw new Error('fail');
- return 'success';
- };
-
- const result = await retryOperation(operation);
-
- expect(result).toBe('success');
- expect(attempts).toBe(3);
- });
- ```
- Clear name, tests real behavior, one thing
- </Good>
-
- <Bad>
- ```typescript
- test('retry works', async () => {
- const mock = jest.fn()
- .mockRejectedValueOnce(new Error())
- .mockRejectedValueOnce(new Error())
- .mockResolvedValueOnce('success');
- await retryOperation(mock);
- expect(mock).toHaveBeenCalledTimes(3);
- });
- ```
- Vague name, tests mock not code
- </Bad>
-
- **Requirements:**
- - One behavior
- - Clear name
- - Real code (no mocks unless unavoidable)
-
- ### Verify RED - Watch It Fail
-
- **MANDATORY. Never skip.**
-
- ```bash
- npm test path/to/test.test.ts
- ```
+ <skill>
- Confirm:
- - Test fails (not errors)
- - Failure message is expected
- - Fails because feature missing (not typos)
+ <purpose>
+ Write the test first. Watch it fail. Write minimal code to pass. If you didn't watch the test fail, you don't know if it tests the right thing.
+ </purpose>
- **Test passes?** You're testing existing behavior. Fix test.
+ <triggers>
+ <rule>Use when implementing any new feature, before writing implementation code.</rule>
+ <rule>Use when fixing any bug — write a failing test that reproduces it first.</rule>
+ <rule>Use when refactoring or changing behavior in existing code.</rule>
+ </triggers>
- **Test errors?** Fix error, re-run until it fails correctly.
+ <warning level="hard">
+ NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST. Violating the letter of the rules is violating the spirit. Write code before the test? Delete it. Start over. No exceptions without your human partner's permission.
+ </warning>
- ### GREEN - Minimal Code
+ <rules>
+ <rule>Test-first is non-negotiable. RED before GREEN.</rule>
+ <rule>Wrote code before the test? Delete it. Don't keep it as "reference", don't "adapt" it, don't look at it. Delete means delete. Implement fresh from tests.</rule>
+ <rule>Exceptions (ask your human partner first): throwaway prototypes, generated code, configuration files.</rule>
+ </rules>
- Write simplest code to pass the test.
+ <process>
+ <step>**RED** — Write one minimal failing test showing what should happen. One behavior, clear name, real code (no mocks unless unavoidable).</step>
+ <step>**Verify RED** — Run `npm test path/to/test.test.ts`. Confirm: test *fails* (not errors), failure message is expected, fails because feature is missing (not typos). Test passes immediately? You're testing existing behavior — fix the test.</step>
+ <step>**GREEN** — Write the simplest code to pass the test. Don't add features, refactor other code, or "improve" beyond the test.</step>
+ <step>**Verify GREEN** — Run tests. Confirm: test passes, *all other* tests still pass, output is pristine (no errors, warnings). Test fails? Fix code, not test. Other tests fail? Fix now.</step>
+ <step>**REFACTOR** — After green only: remove duplication, improve names, extract helpers. Keep tests green. Don't add behavior.</step>
+ <step>**Repeat** — Next failing test for next behavior.</step>
+ </process>
- <Good>
- ```typescript
- async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
- for (let i = 0; i < 3; i++) {
- try {
- return await fn();
- } catch (e) {
- if (i === 2) throw e;
+ <flow format="dot">
+ ```dot
+ digraph tdd_cycle {
+ rankdir=LR;
+ red [label="RED\nFailing test", shape=box, style=filled, fillcolor="#ffcccc"];
+ green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
+ refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
+ red -> green -> refactor -> red;
}
- }
- throw new Error('unreachable');
- }
- ```
- Just enough to pass
- </Good>
-
- <Bad>
- ```typescript
- async function retryOperation<T>(
- fn: () => Promise<T>,
- options?: {
- maxRetries?: number;
- backoff?: 'linear' | 'exponential';
- onRetry?: (attempt: number) => void;
- }
- ): Promise<T> {
- // YAGNI
- }
- ```
- Over-engineered
- </Bad>
-
- Don't add features, refactor other code, or "improve" beyond the test.
-
- ### Verify GREEN - Watch It Pass
-
- **MANDATORY.**
-
- ```bash
- npm test path/to/test.test.ts
- ```
-
- Confirm:
- - Test passes
- - Other tests still pass
- - Output pristine (no errors, warnings)
-
- **Test fails?** Fix code, not test.
-
- **Other tests fail?** Fix now.
-
- ### REFACTOR - Clean Up
-
- After green only:
- - Remove duplication
- - Improve names
- - Extract helpers
-
- Keep tests green. Don't add behavior.
-
- ### Repeat
-
- Next failing test for next feature.
-
- ## Good Tests
-
- | Quality | Good | Bad |
- |---------|------|-----|
- | **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
- | **Clear** | Name describes behavior | `test('test1')` |
- | **Shows intent** | Demonstrates desired API | Obscures what code should do |
-
- ## Why Order Matters
-
- **"I'll write tests after to verify it works"**
-
- Tests written after code pass immediately. Passing immediately proves nothing:
- - Might test wrong thing
- - Might test implementation, not behavior
- - Might miss edge cases you forgot
- - You never saw it catch the bug
-
- Test-first forces you to see the test fail, proving it actually tests something.
-
- **"I already manually tested all the edge cases"**
-
- Manual testing is ad-hoc. You think you tested everything but:
- - No record of what you tested
- - Can't re-run when code changes
- - Easy to forget cases under pressure
- - "It worked when I tried it" ≠ comprehensive
-
- Automated tests are systematic. They run the same way every time.
-
- **"Deleting X hours of work is wasteful"**
-
- Sunk cost fallacy. The time is already gone. Your choice now:
- - Delete and rewrite with TDD (X more hours, high confidence)
- - Keep it and add tests after (30 min, low confidence, likely bugs)
-
- The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
-
- **"TDD is dogmatic, being pragmatic means adapting"**
-
- TDD IS pragmatic:
- - Finds bugs before commit (faster than debugging after)
- - Prevents regressions (tests catch breaks immediately)
- - Documents behavior (tests show how to use code)
- - Enables refactoring (change freely, tests catch breaks)
-
- "Pragmatic" shortcuts = debugging in production = slower.
-
- **"Tests after achieve the same goals - it's spirit not ritual"**
-
- No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
-
- Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
-
- Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
-
- 30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
-
- ## Common Rationalizations
-
- | Excuse | Reality |
- |--------|---------|
- | "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
- | "I'll test after" | Tests passing immediately prove nothing. |
- | "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
- | "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
- | "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
- | "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
- | "Need to explore first" | Fine. Throw away exploration, start with TDD. |
- | "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
- | "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
- | "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
- | "Existing code has no tests" | You're improving it. Add tests for existing code. |
-
- ## Red Flags - STOP and Start Over
-
- - Code before test
- - Test after implementation
- - Test passes immediately
- - Can't explain why test failed
- - Tests added "later"
- - Rationalizing "just this once"
- - "I already manually tested it"
- - "Tests after achieve the same purpose"
- - "It's about spirit not ritual"
- - "Keep as reference" or "adapt existing code"
- - "Already spent X hours, deleting is wasteful"
- - "TDD is dogmatic, I'm being pragmatic"
- - "This is different because..."
-
- **All of these mean: Delete code. Start over with TDD.**
-
- ## Example: Bug Fix
-
- **Bug:** Empty email accepted
+ ```
+ </flow>
- **RED**
- ```typescript
- test('rejects empty email', async () => {
- const result = await submitForm({ email: '' });
- expect(result.error).toBe('Email required');
- });
- ```
+ <example type="good">
+ **RED** — Bug: empty email accepted
+ ```typescript
+ test('rejects empty email', async () => {
+ const result = await submitForm({ email: '' });
+ expect(result.error).toBe('Email required');
+ });
+ ```
+ **Verify RED** — `FAIL: expected 'Email required', got undefined`
- **Verify RED**
- ```bash
- $ npm test
- FAIL: expected 'Email required', got undefined
- ```
+ **GREEN**
+ ```typescript
+ function submitForm(data: FormData) {
+ if (!data.email?.trim()) return { error: 'Email required' };
+ // ...
+ }
+ ```
+ **Verify GREEN** — `PASS`
- **GREEN**
- ```typescript
- function submitForm(data: FormData) {
- if (!data.email?.trim()) {
- return { error: 'Email required' };
- }
- // ...
- }
- ```
+ **REFACTOR** — Extract validation for multiple fields if needed.
+ </example>
- **Verify GREEN**
- ```bash
- $ npm test
- PASS
- ```
+ <example type="bad">
+ Writing production code first, then writing tests that pass immediately. Passing immediately proves nothing — you never saw the test catch the bug.
+ </example>
- **REFACTOR**
- Extract validation for multiple fields if needed.
+ <antipattern>
+ "I'll write tests after to verify it works" — Tests-after answer "What does this do?" Tests-first answer "What *should* this do?" Tests-after are biased by your implementation. You test what you built, not what's required.
+ </antipattern>
- ## Verification Checklist
+ <antipattern>
+ "Keep as reference, write tests first" — You'll adapt it. That's testing after. Delete means delete.
+ </antipattern>
- Before marking work complete:
+ ## Common Rationalizations
- - [ ] Every new function/method has a test
- - [ ] Watched each test fail before implementing
- - [ ] Each test failed for expected reason (feature missing, not typo)
- - [ ] Wrote minimal code to pass each test
- - [ ] All tests pass
- - [ ] Output pristine (no errors, warnings)
- - [ ] Tests use real code (mocks only if unavoidable)
- - [ ] Edge cases and errors covered
+ | Excuse | Reality |
+ |--------|---------|
+ | "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
+ | "I'll test after" | Tests passing immediately prove nothing. |
+ | "Already manually tested" | Ad-hoc != systematic. No record, can't re-run. |
+ | "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is tech debt. |
+ | "Need to explore first" | Fine. Throw away exploration, start with TDD. |
+ | "Test hard = design unclear" | Listen to the test. Hard to test = hard to use. |
+ | "TDD will slow me down" | TDD is faster than debugging. |
+ | "This is different because..." | No. Start over with TDD. |
- Can't check all boxes? You skipped TDD. Start over.
+ ## Red Flags — STOP and Start Over
- ## When Stuck
+ - Code before test
+ - Test passes immediately
+ - Can't explain why test failed
+ - Tests added "later"
+ - Rationalizing "just this once"
+ - "It's about spirit not ritual"
- | Problem | Solution |
- |---------|----------|
- | Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
- | Test too complicated | Design too complicated. Simplify interface. |
- | Must mock everything | Code too coupled. Use dependency injection. |
- | Test setup huge | Extract helpers. Still complex? Simplify design. |
+ **All of these mean: Delete code. Start over with TDD.**
- ## Debugging Integration
+ ## When Stuck
- Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
+ | Problem | Solution |
+ |---------|----------|
+ | Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
+ | Test too complicated | Design too complicated. Simplify interface. |
+ | Must mock everything | Code too coupled. Use dependency injection. |
- Never fix bugs without a test.
+ ## Verification Checklist
- ## Testing Anti-Patterns
+ Before marking work complete:
- When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
- - Testing mock behavior instead of real behavior
- - Adding test-only methods to production classes
- - Mocking without understanding dependencies
+ - [ ] Every new function/method has a test
+ - [ ] Watched each test fail before implementing
+ - [ ] Each test failed for expected reason (feature missing, not typo)
+ - [ ] Wrote minimal code to pass each test
+ - [ ] All tests pass with pristine output
+ - [ ] Tests use real code (mocks only if unavoidable)
+ - [ ] Edge cases and errors covered
- ## Final Rule
+ Can't check all boxes? You skipped TDD. Start over.
- ```
- Production code → test exists and failed first
- Otherwise → not TDD
- ```
+ <include ref="_shared/red-flags-skill-discipline"/>
+ <see file="testing-anti-patterns.md"/>
- No exceptions without your human partner's permission.
+ </skill>