Tracked: CLAUDE.md, agents, skills, settings, memory. Ephemeral data (sessions, history, telemetry, tasks) excluded via .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.2 KiB
Test-Driven Development
The Core Loop
RED → GREEN → REFACTOR → RED → ...
RED Phase
Write a failing test that describes the behavior you want. The test should:
- Use domain language, not technical jargon
- Describe WHAT, not HOW
- Be a concrete example, not an abstract statement
// BAD: Abstract
it('can add numbers', () => { ... });
// GOOD: Concrete example
it('when adding 2 + 3, returns 5', () => { ... });
GREEN Phase
Write the simplest possible code to make the test pass. Two strategies:
-
Fake It - Return a hardcoded value
add(a: number, b: number): number { return 5; // Simplest thing! } -
Obvious Implementation - If you know the solution
add(a: number, b: number): number { return a + b; }
Prefer Fake It when learning or unsure. Let more tests drive the real implementation.
REFACTOR Phase
This is where design happens. Look for:
- Duplication (but wait for Rule of Three)
- Long methods to extract
- Poor names to improve
- Complex conditions to simplify
The Three Laws of TDD
- No production code without a failing test
- No more test code than sufficient to fail (compilation failures count)
- No more production code than sufficient to pass the one failing test
The Rule of Three
Only extract duplication when you see it THREE times.
Why? Wrong abstractions are worse than duplication. Wait for the pattern to emerge.
// Duplication #1 - Leave it
// Duplication #2 - Note it, leave it
// Duplication #3 - NOW extract it
Triangulation
Each new test "sculpts" the solution toward a general, robust implementation.
Think of degrees of freedom - like a car that needs forward/back, left/right, and rotation. Each test carves out one degree of freedom until the implementation handles all cases.
Transformation Priority Premise
When going from RED to GREEN, prefer simpler transformations:
| Priority | Transformation |
|---|---|
| 1 | {} → nil |
| 2 | nil → constant |
| 3 | constant → variable |
| 4 | unconditional → conditional |
| 5 | scalar → collection |
| 6 | statement → recursion |
| 7 | value → mutated value |
Higher priority = simpler. Avoid jumping to complex transformations too early.
Arrange-Act-Assert
Structure every test:
it('calculates total with discount', () => {
// ARRANGE - Set up the world
const order = new Order();
order.addItem({ price: 100 });
const discount = new PercentDiscount(10);
// ACT - Execute the behavior
const total = order.calculateTotal(discount);
// ASSERT - Verify the outcome
expect(total).toBe(90);
});
Writing Tests Backwards
Sometimes it helps to write AAA in reverse:
- Write the ASSERT first - what do you want to verify?
- Write the ACT - what action produces that result?
- Write the ARRANGE - what setup is needed?
Test Naming Principles
- Use behavior-driven names with domain language
- Provide concrete examples, not abstract statements
- One example per test for easy debugging
- Avoid leaking implementation details
// BAD: Technical, implementation-focused
it('should set the data property to 1', () => { ... });
// GOOD: Behavior-focused, domain language
it('should recognize "mom" as a palindrome', () => { ... });
Classic vs Mockist TDD
Classic (Detroit/Chicago) TDD:
- Test with real dependencies
- Higher confidence, slower tests
- Best for: Pure functions, integration tests
Mockist (London) TDD:
- Mock external dependencies
- Faster tests, more isolated
- Best for: Classes with infrastructure dependencies
Start with Classic TDD to learn the technique. Add mocks when testing code with databases, APIs, etc.
Common Mistakes
- Writing code before tests - Violates the fundamental principle
- Writing too much test - Just enough to fail
- Writing too much code - Just enough to pass
- Skipping refactor - This is where design lives
- Testing implementation - Test behavior, not how it's done
- Abstract test names - Use concrete examples
- Extracting too early - Wait for Rule of Three