--- 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 ```bash # 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: ```typescript // 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: ```typescript 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: ```typescript 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: ```typescript // 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 1. **Writing too many tests at once** - One failing test at a time 2. **Testing implementation, not behavior** - Test what, not how 3. **Skipping the refactor step** - Technical debt accumulates 4. **Over-mocking** - Don't mock what you don't own 5. **Testing private methods** - Test through public interface ## Integration with Beads When working on a bead: 1. Start bead: `beads_start(id="bd-123")` 2. Write failing test for the requirement 3. Implement to pass 4. Refactor 5. Close bead: `beads_close(id="bd-123", reason="Done: tests passing")`