tdd · diff

git:20260619.4f06aed to git:20260707.1629338

16 added, 88 removed. Audit A to A.

---
name: tdd
description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
---
# Test-Driven Development
- ## Philosophy
+ TDD is the red → green loop. This skill is the reference that makes that loop produce tests worth keeping: what a good test is, where tests go, the anti-patterns, and the rules of the loop. Every section applies on every cycle — consult them before and during the loop, not after.
- **Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
+ When exploring the codebase, read `CONTEXT.md` (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
- **Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
+ ## What a good test is
- **Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
+ Tests verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. A good test reads like a specification — "user can checkout with valid cart" tells you exactly what capability exists — and survives refactors because it doesn't care about internal structure.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
- ## Anti-Pattern: Horizontal Slices
-
- **DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
-
- This produces **crap tests**:
-
- - Tests written in bulk test _imagined_ behavior, not _actual_ behavior
- - You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
- - Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
- - You outrun your headlights, committing to test structure before understanding the implementation
-
- **Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
-
- ```
- WRONG (horizontal):
- RED: test1, test2, test3, test4, test5
- GREEN: impl1, impl2, impl3, impl4, impl5
-
- RIGHT (vertical):
- RED→GREEN: test1→impl1
- RED→GREEN: test2→impl2
- RED→GREEN: test3→impl3
- ...
- ```
-
- ## Workflow
-
- ### 1. Planning
-
- When exploring the codebase, read `CONTEXT.md` (if it exists) so that test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
-
- Before writing any code:
-
- - [ ] Confirm with user what interface changes are needed
- - [ ] Confirm with user which behaviors to test (prioritize)
- - [ ] Identify opportunities for deep modules (small interface, deep implementation) — run the `/codebase-design` skill for the vocabulary and the testability checks
- - [ ] List the behaviors to test (not implementation steps)
- - [ ] Get user approval on the plan
-
- Ask: "What should the public interface look like? Which behaviors are most important to test?"
-
- **You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
-
- ### 2. Tracer Bullet
-
- Write ONE test that confirms ONE thing about the system:
-
- ```
- RED: Write test for first behavior → test fails
- GREEN: Write minimal code to pass → test passes
- ```
-
- This is your tracer bullet - proves the path works end-to-end.
-
- ### 3. Incremental Loop
-
- For each remaining behavior:
-
- ```
- RED: Write next test → fails
- GREEN: Minimal code to pass → passes
- ```
-
- Rules:
+ ## Seams — where tests go
- - One test at a time
- - Only enough code to pass current test
- - Don't anticipate future tests
- - Keep tests focused on observable behavior
+ A **seam** is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
- ### 4. Refactor
+ **Test only at pre-agreed seams.** Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything — agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
- After all tests pass, look for [refactor candidates](refactoring.md):
+ Ask: "What's the public interface, and which seams should we test?"
- - [ ] Extract duplication
- - [ ] Deepen modules (move complexity behind simple interfaces)
- - [ ] Apply SOLID principles where natural
- - [ ] Consider what new code reveals about existing code
- - [ ] Run tests after each refactor step
+ ## Anti-patterns
- **Never refactor while RED.** Get to GREEN first.
+ - **Implementation-coupled** — mocks internal collaborators, tests private methods, or verifies through a side channel (querying the database instead of using the interface). The tell: the test breaks when you refactor but behavior hasn't changed.
+ - **Tautological** — the assertion recomputes the expected value the way the code does (`expect(add(a, b)).toBe(a + b)`, a snapshot derived by hand the same way, a constant asserted equal to itself), so it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth — a known-good literal, a worked example, the spec.
+ - **Horizontal slicing** — writing all tests first, then all implementation. Bulk tests verify _imagined_ behavior: you test the _shape_ of things rather than user-facing behavior, the tests go insensitive to real changes, and you commit to test structure before understanding the implementation. Work in **vertical slices** instead — one test → one implementation → repeat, each test a **tracer bullet** that responds to what the last cycle taught you.
- ## Checklist Per Cycle
+ ## Rules of the loop
- ```
- [ ] Test describes behavior, not implementation
- [ ] Test uses public interface only
- [ ] Test would survive internal refactor
- [ ] Code is minimal for this test
- [ ] No speculative features added
- ```
+ - **Red before green.** Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features.
+ - **One slice at a time.** One seam, one test, one minimal implementation per cycle.
+ - **Refactoring is not part of the loop.** It belongs to the review stage (see the `code-review` skill), not the red → green implementation cycle.