From e81f97660af0bebfdbf2e23db6a71f7dfcb9a659 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 30 Jun 2026 09:43:49 +0100 Subject: [PATCH] tdd: reshape into reference-only with pre-agreed seams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The red → green → refactor loop is anchored by leading words the model already holds, so the step-by-step Workflow mostly restated the loop and duplicated the horizontal-slicing anti-pattern. Drop the Workflow and per-cycle checklist; fold their durable idea (vertical slices / tracer bullets) into Anti-patterns and a short Rules-of-the-loop list. Introduce **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. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/tdd-reference-only-seams.md | 5 ++ skills/engineering/tdd/SKILL.md | 107 ++++--------------------- 2 files changed, 21 insertions(+), 91 deletions(-) create mode 100644 .changeset/tdd-reference-only-seams.md diff --git a/.changeset/tdd-reference-only-seams.md b/.changeset/tdd-reference-only-seams.md new file mode 100644 index 0000000..f29550d --- /dev/null +++ b/.changeset/tdd-reference-only-seams.md @@ -0,0 +1,5 @@ +--- +"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. diff --git a/skills/engineering/tdd/SKILL.md b/skills/engineering/tdd/SKILL.md index 23441dd..6bd9fb6 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 → refactor 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. +- **Never refactor while red.** Get to green first, then look for [refactor candidates](refactoring.md) — including deepening modules (small interface, deep implementation; see `/codebase-design`). Run the tests after each refactor step.