tdd · diff

git:20260928.0bbe6db to git:20260930.664568a

35 added, 76 removed. Audit A to A.

---
name: tdd
description: Use before implementing features or bug fixes with testable behavior, including requests that do not mention tests or TDD. Not for behavior-preserving refactors or prose, styling, and static-content edits.
---
# Test-Driven Development
- TDD is a sequence of small red → green → refactor cycles. Make the intended behavior executable and observe its failure before implementing it. A completed cycle leaves the affected path in a production-quality shape; a blocked cycle preserves the valid failing test and reports what prevents completion.
-
- 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.
-
- ## What a good test is
-
- Tests protect a meaningful contract through an interface or observation that can detect a realistic regression. Prefer tests that survive implementation-only refactors of that contract. A test such as "user can checkout with valid cart" explains the capability it protects; changing the contract itself may legitimately require changing its tests.
-
- See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
-
- ## Seams: where tests go
+ Work in small red → green → refactor cycles: write one behavior as a test, watch it fail, implement it, then tidy the changed code. Read `CONTEXT.md` and relevant ADRs if they exist, so tests use the project's domain terms.
- A **seam** is a boundary where a test exercises behavior or controls a dependency. It may be a public API or an internal interface owned by a module. An internal module can have its own contract and meaningful tests without exposing that interface to application callers. Do not bypass encapsulation merely to assert on incidental private state.
+ ## Choose the test
- Before writing a test, identify the requirement that establishes the expected outcome, the starting conditions, and the realistic failure it should catch. Then choose where to call the code or control its dependencies. Prefer existing interfaces and nearby test conventions. Use an internal interface when its tests catch important failures that other tests would miss. Do not expose private state or add an abstraction just to make a test easier to write. Ask only when the choice would change the agreed scope or a contract.
+ Before writing a test, name the requirement that sets the expected outcome, the starting conditions, and the realistic failure the test should catch.
- Testing effort should land on critical paths and complex logic rather than every edge case.
+ - Test through the interface that owns the behavior. This can be a public API or an internal module interface. Prefer existing interfaces and the test conventions near the code.
+ - Use real collaborators when their behavior is part of what the test claims. Replace a dependency only to control time, randomness, or failures, or when it sits outside the test's claim. [test-evidence.md](test-evidence.md) lists what counts as evidence for common contracts and when test doubles are appropriate.
+ - Put effort into critical paths and complex logic, not every edge case.
+ - Do not expose private state or add public methods, wrappers, or configuration only to make a test easier to write. When the interface itself is in question, use the `codebase-design` skill.
- When the shape of the interface is itself in question—how deep the module is, where the seam belongs, or what the interface should expose—use the `codebase-design` skill for the shared module and seam vocabulary.
+ ## Cycle
- ## Anti-patterns
+ Features and bug fixes follow the same cycle. For a bug, the first test reproduces the reported failure at the narrowest level already tested near that code.
- - **Implementation-coupled**: asserts incidental private state, helper calls, or ordering that the tested contract does not promise. Internal tests, collaborator doubles, and direct database observations are not inherently coupled; judge whether they detect a real contract failure or merely freeze the implementation's shape.
- - **Tautological or non-independent expectations**: an assertion that derives its expected value from the result under test cannot provide independent evidence. Copying production logic into the expected-value calculation can also reproduce the same bug. Use expectations grounded independently in the contract, a worked example, or a trusted oracle; a calculated expectation is not automatically tautological.
- - **Writing all tests before any implementation:** this can commit the tests to a design before trying it in working code. Write one test, implement its behavior, and use what you learn to choose the next test.
+ 1. Pick the smallest observable behavior.
+ 2. Write one focused test for it.
+ 3. Run it before changing production code, and confirm it fails because the behavior is missing. If it passes, or fails because of a setup or test error, fix the test first.
+ 4. Make the simplest production-quality change that passes it. Don't add behavior for later tests.
+ 5. Run it and confirm it passes.
+ 6. Refactor the changed code if ownership or cohesion suffered, and rerun the test.
+ 7. Choose the next behavior based on what you learned. Don't write all the tests up front.
- ## Feature cycle
+ For a flaky bug, make the regression test deterministic and say what it locks down. If the bug belongs to a broader class of failures, fix the reported case first, then consider sibling cases.
- Work in vertical slices:
+ ## Keep the expectation honest
- 1. Choose the smallest observable behavior at an established or agreed seam.
- 2. Write one focused test that specifies that behavior.
- 3. Run it before changing production code. Confirm that it fails because the behavior is missing, not because the test is broken.
- 4. Make the simplest production-quality change that passes the test without adding future behavior. Extract a focused module when it makes responsibilities clearer, even if it has only one caller.
- 5. Rerun the test and confirm that it passes.
- 6. Refactor the affected path when needed to preserve clear ownership and cohesion, then confirm the test still passes.
- 7. Repeat with the next behavior, allowing each cycle to inform the next.
+ Change a test's expected outcome only when the requirement changed, the test was wrong against the requirement, or an interface change keeps the same protected behavior. Say why when you do. The implementation's new output is never a reason to change the expected output.
- ## Bug-fix cycle
+ These moves make a failing test pass without fixing the behavior. Do not use them:
- When a bug has a clear, practical regression path:
+ - updating an assertion, snapshot, or fixture to match the new output
+ - seeding state in setup that the real workflow does not guarantee, such as a flag, a backfill, or initialized data
+ - mocking away the logic or integration the test claims to check
+ - accepting an error as success, or testing only graceful rejection when the requirement is a working path
+ - skipping the test, marking it as an expected failure, or excluding it from the run
+ - computing the expected value with the same logic as the production code
- 1. Identify the intended behavior, current behavior, affected path, and smallest observable reproduction.
- 2. Choose the narrowest executable check already used near that codepath.
- 3. Add the smallest focused regression test that would have caught the bug.
- 4. Run it before fixing the implementation. Confirm that it fails for the intended reason; correct the test or reproduction if it passes or fails for an unrelated reason.
- 5. Make a focused production-quality change that restores the intended behavior while preserving nearby contracts and clear ownership.
- 6. Rerun the regression test, then run relevant adjacent tests, type checks, lint, or scenario checks in proportion to the change's risk.
+ If a valid failing test needs a domain decision, a missing prerequisite, or work outside the agreed scope, keep the test red, keep the completed work, and report the blocker. The work is unfinished. A request only to reproduce a bug is complete with the test still red and does not authorize fixing production code.
## When a failing test is impractical
- Do not create substantial test infrastructure, brittle mocks, slow end-to-end setup, production-only state, or broad fixture churn merely to satisfy the workflow. Prefer no new test over a test with weak or misleading signal.
-
- Before implementing the behavior, explain the concrete limitation and choose a check that can demonstrate the missing behavior and its correction. This may be a script, manual command, browser workflow, log assertion, or integration check. It must exercise the required behavior, not merely nearby code that already works. If execution is unavailable, report the evidence gap; do not substitute a test that bypasses it.
-
- A test added after implementation can protect against a regression, but is not evidence of test-first work. If it already passes, inspect whether the behavior exists or the check misses it. Do not manufacture a failure just to claim a red phase. Running a later regression against the old implementation can establish sensitivity; report that sequence accurately.
-
- ## Preserve the expectation
-
- Change a test only when the authorized requirement changed, evidence shows the test was wrong against that requirement, or an interface/setup change preserves the same protected behavior. State the reason for a material expectation change. The implementation's new output is not authority to change its expected output.
-
- This applies to assertions, fixtures, mocks, snapshots, skips, expected-failure markers, and test selection. Do not make a failing case pass by removing its relevant starting conditions, accepting its error as success, or bypassing the failing collaborator. When replacing a test, retain its meaningful failure cases unless their contract was explicitly retired.
-
- Distinguish a test defect, unavailable execution, and a real contract failure. Fix a broken reproduction without changing the intended outcome. A setup or infrastructure error is not the required behavioral red; preserve and report it separately if it prevents reaching that behavior.
-
- ## When green is blocked
-
- Continue authorized implementation and necessary restructuring while they can satisfy the expectation. Refactoring after green does not prohibit structural changes needed to reach green.
-
- If a valid failure requires an unresolved domain decision, unavailable prerequisite, or material scope expansion, preserve the failing test and completed work. Report the expected and actual behavior, the command and failure observed, and the smallest prerequisite or decision needed. Continue independent in-scope work where useful. Do not weaken the test or disable the feature to make validation pass.
-
- A pre-existing defect can still block the requested behavior; its age does not make acceptance optional. A blocked implementation is unfinished. A request only to reproduce a bug or add a failing regression can be complete with that test still red; it does not authorize fixing production code.
-
- ## Validation cadence
-
- Within each red → green → refactor cycle, run the focused check and any adjacent checks affected by the current change. Run broader validation when a shared contract, integration risk, failure, or repository rule makes it necessary, not automatically after every slice.
-
- At completion, satisfy applicable repository checks and meaningful regression coverage. Reuse results that still cover the final implementation and relevant conditions; rerun only invalidated or failed checks unless a repository rule requires otherwise. Do not repeat a full suite, build, or lint pass merely to mark another cycle complete.
+ Don't build substantial test infrastructure, slow end-to-end setups, or elaborate mocks just to follow the cycle. Before implementing, explain the limitation and pick another check that shows the behavior is missing and then fixed, such as a script, a command, a browser workflow, or a log check. If nothing can run, report the gap.
- Verify that the relevant tests actually ran under the reported command. Report passed, failed, skipped, and unable-to-run checks distinctly. Required coverage hidden behind an opt-in or missing prerequisite remains a verification gap, even when the command exits successfully.
+ A test written after the implementation still protects against regressions, but it is not evidence of test-first work. Don't fake a red phase. If you run the test against the old code to show it would have failed, report it that way.
- ## Guardrails
+ ## Validation
- - **Red before green.** Write the failing test first, then implement the current behavior completely. Don't anticipate future tests or add speculative features.
- - **One behavior at a time.** Keep each cycle focused on one coherent contract change. Use the observations and dependency controls needed to prove it without batching unrelated behavior into the same cycle.
- - Keep a regression test focused on the reported bug; avoid unrelated coverage expansion or fixture churn.
- - If a bug is flaky, make the regression signal deterministic where practical and state what signal is being locked down.
- - If a bug exposes a broader class of failures, establish the focused regression path first, then consider sibling coverage.
- - **Refactor after green when the changed path needs it.** Preserve clear ownership and cohesion before starting the next slice. Keep the refactor within the affected behavior; do not turn the cycle into unrelated cleanup.
+ In each cycle, run the focused test and tests near the change. Run broader checks when a shared contract or a repository rule calls for it, and run the repository's required checks once at the end. Confirm the tests you rely on actually ran; a passing command that skipped them is a gap.
## Final response
- Report the evidence, not only the outcome:
-
- - Name the failing-before test or executable check and the failure it produced.
- - Name the passing-after test run, or the still-failing regression and blocker, with any nearby validation performed.
- - If failing-before evidence could not be demonstrated, state why and describe the closest regression check used instead.
+ - the test or check that failed before the change, and how it failed
+ - the passing run after the change, or the still-failing test and its blocker
+ - other checks run, with results
+ - if failing-before evidence was not possible, why, and which check was used instead