diff --git a/.changeset/tdd-reference-only-seams.md b/.changeset/tdd-reference-only-seams.md new file mode 100644 index 0000000..6696d46 --- /dev/null +++ b/.changeset/tdd-reference-only-seams.md @@ -0,0 +1,7 @@ +--- +"mattpocock-skills": patch +--- + +Reshape the `tdd` skill into reference-only. The red → green → refactor loop is anchored by leading words the model already holds, so the step-by-step Workflow was largely restating the loop and duplicating the horizontal-slicing anti-pattern. Dropped the Workflow and per-cycle checklist; folded their one durable idea — vertical slices / tracer bullets — into the Anti-patterns section and a short Rules-of-the-loop list. Introduced **seam** as the leading word for where tests go, collapsing the old Philosophy "public interfaces" prose and the Planning "confirm interface / behaviors" handshake into one rule: test only at pre-agreed seams, confirmed with the user before any test is written. + +Also dropped the refactor stage — TDD is now red → green, not red → green → refactor. Refactoring belongs to the review stage, not the implementation loop, so the refactor rule and `refactoring.md` were removed (its home is the `review` skill). diff --git a/skills/engineering/tdd/SKILL.md b/skills/engineering/tdd/SKILL.md index 23441dd..4e243fb 100644 --- a/skills/engineering/tdd/SKILL.md +++ b/skills/engineering/tdd/SKILL.md @@ -5,107 +5,32 @@ description: Test-driven development. Use when the user wants to build features # 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. - -**Tautological tests** restate the implementation inside the assertion, so they pass by construction and give zero confidence. When the expected value is computed the way the code computes it — `expect(add(a, b)).toBe(a + b)`, snapshotting a figure you derived by hand the same way the code does, asserting a constant equals itself — the test can never disagree with the code: break the code wrong and the assertion breaks wrong with it. The expected value must come from an independent source of truth — a known-good literal, a worked example, the spec. +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 +## Seams — where tests go -**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." +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. -This produces **crap tests**: +**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. -- 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 +Ask: "What's the public interface, and which seams should we test?" -**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. +## Anti-patterns -``` -WRONG (horizontal): - RED: test1, test2, test3, test4, test5 - GREEN: impl1, impl2, impl3, impl4, impl5 +- **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. -RIGHT (vertical): - RED→GREEN: test1→impl1 - RED→GREEN: test2→impl2 - RED→GREEN: test3→impl3 - ... -``` +## Rules of the loop -## 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: - -- One test at a time -- Only enough code to pass current test -- Don't anticipate future tests -- Keep tests focused on observable behavior - -### 4. Refactor - -After all tests pass, look for [refactor candidates](refactoring.md): - -- [ ] 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 - -**Never refactor while RED.** Get to GREEN first. - -## Checklist Per Cycle - -``` -[ ] Test describes behavior, not implementation -[ ] Test uses public interface only -[ ] Test would survive internal refactor -[ ] Expected values are independent literals, not recomputed from the code -[ ] 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 `review` skill), not the red → green implementation cycle. diff --git a/skills/engineering/tdd/refactoring.md b/skills/engineering/tdd/refactoring.md deleted file mode 100644 index 8a44439..0000000 --- a/skills/engineering/tdd/refactoring.md +++ /dev/null @@ -1,10 +0,0 @@ -# Refactor Candidates - -After TDD cycle, look for: - -- **Duplication** → Extract function/class -- **Long methods** → Break into private helpers (keep tests on public interface) -- **Shallow modules** → Combine or deepen -- **Feature envy** → Move logic to where data lives -- **Primitive obsession** → Introduce value objects -- **Existing code** the new code reveals as problematic