| name | tdd |
| description | Test-Driven Development workflow with RED-GREEN-REFACTOR, lore from Kent Beck, Michael Feathers, and Ousterhout's counterpoint |
| tags | testing, workflow, methodology, tdd |
Test-Driven Development (TDD)
The Rhythm: RED-GREEN-REFACTOR
1. RED - Write failing test first (define expected behavior)
2. GREEN - Minimal implementation to pass (don't over-engineer)
3. REFACTOR - Clean up, remove duplication, run tests again
Why TDD Works (The Lore)
Kent Beck (Test-Driven Development by Example)
"The act of writing a unit test is more an act of design than verification."
- Tests become executable documentation of intent
- "Fake it til you make it" - start with hardcoded values, generalize
- Small steps reduce debugging time
- Confidence to refactor comes from test coverage
Michael Feathers (Working Effectively with Legacy Code)
"The most powerful feature-addition technique I know of is test-driven development."
- TDD works in both OO and procedural code
- Writing tests first forces you to think about interfaces
- Tests are the safety net that enables aggressive refactoring
- Legacy code = code without tests
Martin Fowler (Refactoring)
"Kent Beck baked this habit of writing the test first into a technique called Test-Driven Development."
- TDD relies on short cycles
- Tests enable refactoring
- Refactoring becomes safe - tests catch regressions instantly
The Counterpoint: Know When to Break the Rule
John Ousterhout (A Philosophy of Software Design)
"The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design."
When TDD can hurt:
- Can lead to tactical programming (feature-focused, not design-focused)
- May produce code that's easy to test but hard to understand
- Risk of over-testing implementation details
The balance:
- For exploratory/architectural work, design first, then add tests
- Don't let tests drive you into a corner
- Step back periodically to evaluate overall design
When to Use TDD
✅ Use TDD for:
- New features with clear requirements
- Bug fixes (write test that reproduces bug first)
- Refactoring existing code (add characterization tests first)
- API design (tests reveal ergonomics)
- Any code that will be maintained long-term
❌ Skip TDD for:
- Exploratory spikes (but add tests after if keeping the code)
- Emergency hotfixes (but add tests immediately after)
- Pure UI/styling changes
- One-off scripts
- Throwaway prototypes
The TDD Workflow
# 1. Write test, watch it fail
bun test src/thing.test.ts # RED - test fails
# 2. Implement minimal code to pass
bun test src/thing.test.ts # GREEN - test passes
# 3. Refactor, tests still pass
bun test src/thing.test.ts # GREEN - still passing
# 4. Repeat for next behavior
TDD Patterns
Start with the Assertion
Write the assertion first, then work backwards:
// Start here
expect(result).toBe(42);
// Then figure out what 'result' is
const result = calculate(input);
// Then figure out what 'input' is
const input = { value: 21 };
Triangulation
Use multiple examples to drive generalization:
it("doubles 2", () => expect(double(2)).toBe(4));
it("doubles 3", () => expect(double(3)).toBe(6));
// Now you MUST implement the general solution
Obvious Implementation
When the solution is obvious, just write it:
function add(a: number, b: number): number {
return a + b; // Don't fake this
}
Fake It Til You Make It
When unsure, start with hardcoded values:
// First pass
function fibonacci(n: number): number {
return 1; // Passes for n=1
}
// Add test for n=2, then generalize
Testing Pyramid
/\
/ \ E2E (few)
/----\
/ \ Integration (some)
/--------\
/ \ Unit (many)
--------------
- Unit tests: Fast, isolated, test one thing
- Integration tests: Test component interactions
- E2E tests: Test full user flows (expensive, use sparingly)
Common TDD Mistakes
- Writing too many tests at once - One failing test at a time
- Testing implementation, not behavior - Test what, not how
- Skipping the refactor step - Technical debt accumulates
- Over-mocking - Don't mock what you don't own
- Testing private methods - Test through public interface
Integration with Beads
When working on a bead:
- Start bead:
beads_start(id="bd-123") - Write failing test for the requirement
- Implement to pass
- Refactor
- Close bead:
beads_close(id="bd-123", reason="Done: tests passing")