diff --git a/.changeset/tdd-tautological-tests.md b/.changeset/tdd-tautological-tests.md new file mode 100644 index 0000000..782e9de --- /dev/null +++ b/.changeset/tdd-tautological-tests.md @@ -0,0 +1,5 @@ +--- +"mattpocock-skills": patch +--- + +Add the **tautological test** anti-pattern to the `tdd` skill. Tests whose assertion is recomputed the way the code computes it pass by construction and give zero confidence — distinct from the implementation-coupling anti-pattern already covered. Added as a peer at the same three sites: a Philosophy principle (expected values must come from an independent source of truth), a per-cycle checklist gate, and a BAD/GOOD example pair in `tests.md`. diff --git a/skills/engineering/tdd/SKILL.md b/skills/engineering/tdd/SKILL.md index 1ce5d21..23441dd 100644 --- a/skills/engineering/tdd/SKILL.md +++ b/skills/engineering/tdd/SKILL.md @@ -13,6 +13,8 @@ description: Test-driven development. Use when the user wants to build features **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. + See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. ## Anti-Pattern: Horizontal Slices @@ -103,6 +105,7 @@ After all tests pass, look for [refactor candidates](refactoring.md): [ ] 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 ``` diff --git a/skills/engineering/tdd/tests.md b/skills/engineering/tdd/tests.md index ff22f80..7ab8647 100644 --- a/skills/engineering/tdd/tests.md +++ b/skills/engineering/tdd/tests.md @@ -59,3 +59,19 @@ test("createUser makes user retrievable", async () => { expect(retrieved.name).toBe("Alice"); }); ``` + +**Tautological tests**: Expected value restates the implementation, so the test passes by construction. + +```typescript +// BAD: Expected value is recomputed the way the code computes it +test("calculateTotal sums line items", () => { + const items = [{ price: 10 }, { price: 5 }]; + const expected = items.reduce((sum, i) => sum + i.price, 0); + expect(calculateTotal(items)).toBe(expected); +}); + +// GOOD: Expected value is an independent, known literal +test("calculateTotal sums line items", () => { + expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15); +}); +```