| name | follow-tdd |
| description | This skill should be used when implementing any feature, fixing any bug, or changing any code behavior, before writing the implementation code. It also applies when the user says "TDD", "test first", "red-green-refactor", "write the test before the code", when authoring automated end-to-end browser tests (Playwright) for a user journey, and after the fix skill has reproduced a bug's root cause, to write the regression test first. It should not be used to diagnose a reported bug (use fix; it hands off here), for pure configuration changes, documentation, static content, or for runtime-verifying an already-built change (use the verify skill for that). |
Purpose
Drive every behavior change with a failing test written first. Tests are proof; "seems right" is not done. Do not write any production code until a failing test exists for it, and do not declare work complete until the Verification Checklist passes.
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Wrote code before its test? Delete it. Start over.
No exceptions:
- Do not keep it as "reference"
- Do not "adapt" it while writing the tests
- Do not look at it
- Delete means delete
Why this severe: code written first biases the tests toward what you built instead of what is required, and tests written after pass immediately, which proves nothing. The deletion is not punishment; it is the only way the rewritten code is actually derived from the tests.
Violating the letter of this rule is violating its spirit. Arguments that a workaround honors the spirit are the failure mode, not an exception.
The only exceptions, each requiring user confirmation first: throwaway exploration spikes (thrown away before the real implementation starts with TDD) and generated code.
Workflow
Copy this checklist and track each cycle:
TDD Cycle:
- [ ] 1. Behaviors listed and prioritized
- [ ] 2. RED: one failing test written
- [ ] 3. Verify RED: watched it fail for the right reason
- [ ] 4. GREEN: minimal code to pass
- [ ] 5. Verify GREEN: test passes, full suite green
- [ ] 6. REFACTOR: clean up, tests stay green
- [ ] 7. Repeat for next behavior
1. List the behaviors
Before any test, list the behaviors to verify (not implementation steps), using the project's existing domain vocabulary.
For every feature, the coverage floor is three tests, and naming them is how you find the behaviors:
- Golden path: the feature does what it is for, with valid input under expected conditions. The proof it works at all.
- Error case: invalid input or a failure condition is handled as designed (rejected, returns an error, throws, degrades), not swallowed or crashed. The proof it fails safely.
- Edge case: the boundary where the logic is most likely to break (empty, null, zero, the maximum, the off-by-one, the concurrent call, the duplicate). The proof it holds at the corners.
This skill is the canonical home of the three-test floor; other skills and documents that cite golden path, error, and edge coverage reference this definition rather than restating it.
This is a floor, not a ceiling. A feature with branching logic or several failure modes needs an error and an edge test per branch; a trivial pure function may genuinely need only the three. Above the floor, prioritize critical paths and complex logic over exhaustive enumeration, and confirm priorities with the user when the interface design is not already settled. Skipping a category is a decision to state, not a default to drift into: "no error case because the type system makes invalid input unrepresentable" is a valid call, silently testing only the golden path is not.
The three categories are per-behavior; a feature that spans a user-facing flow has one more level to cover. The complete user journey is itself a behavior: the end-to-end path a real user walks to accomplish the goal (arrive, move through each step in realistic order, reach the end state), exercised by an integration or e2e test against the assembled system, not only each unit in isolation. Enumerate the primary journey and any flow where the steps interact or carry state forward, because a suite where every unit passes but the assembled journey was never run has a coverage gap: the flow can break at a seam where each step passes on its own.
2. RED: write one failing test
One test, one behavior, through the public interface.
- One behavior per test; an "and" in the test name means split it
- The name reads like a specification: "rejects empty email", not "test1" or "validation works"
- Real code paths; test doubles only at system boundaries (see references/test-quality.md)
- Assert on outcomes (state), not on which methods were called internally; interaction assertions break on refactors even when behavior is unchanged
3. Verify RED: watch it fail
Mandatory, never skip. Run the test and confirm:
- It fails, not errors: a syntax error or broken import is not a valid RED
- It fails because the feature is missing, with the failure message you expected
If the test passes immediately, you are testing behavior that already exists; fix the test, not the code. If you cannot explain why it failed, you do not know what it tests.
4. GREEN: minimal code to pass
Write the simplest code that makes this test pass. Do not anticipate future tests, add options, or improve neighboring code; speculative generality is how minimal implementations become over-engineered ones (YAGNI).
5. Verify GREEN: watch it pass
Run the test: it passes. Run the full suite: nothing else broke. Output is pristine: no new errors or warnings. If the test fails, fix the code, not the test. Do not re-run an unchanged passing suite for reassurance; re-run only after edits.
6. REFACTOR: clean up on green only
Remove duplication, improve names, extract helpers. Run tests after each refactor step; behavior does not change. Never refactor while RED.
7. Repeat
Next behavior, next failing test. Each cycle is a complete vertical slice.
Vertical Slices, Not Horizontal
Do not write all the tests first and then all the implementation:
WRONG (horizontal): test1, test2, test3 ... then impl1, impl2, impl3
RIGHT (vertical): test1 -> impl1, test2 -> impl2, test3 -> impl3
Tests written in bulk test imagined behavior, not actual behavior; you commit to test structure before the implementation has taught you anything. Each vertical slice is a tracer bullet: one test, one implementation, then the next test responds to what that cycle revealed. This is the same one-behavior-at-a-time ordering plans use, so a plan that lists work in tracer-bullet order is implemented in that same order, not batched into all-tests-then-all-code.
The Prove-It Pattern (Bug Fixes)
When a bug arrives, do not start with the fix. Write a test that reproduces the bug and watch it fail; that failure is the proof the bug exists and the proof your fix works when it flips to green. Then run the full suite for regressions. Never fix a bug without a reproduction test: an untested fix can be a coincidence, and the regression returns silently.
For complex bugs, consider spawning a subagent to write the reproduction test from the bug report alone, without knowledge of the planned fix; tests written blind to the fix are harder to accidentally bias.
Good Tests
| Quality | Do | Not |
|---|
| Minimal | One behavior per test | "and" in the name |
| Specified | Name describes expected behavior | "works", "handles errors" |
| Behavioral | Public interface, observable outcomes | Private methods, call sequences |
| Self-contained | DAMP: each test readable alone | Setup buried in shared helpers |
| Structured | Arrange-Act-Assert | Interleaved setup and assertions |
| Real | Real implementation > fake > stub > mock | Mocking your own modules |
Test Anti-Patterns
These failures make the suite pass while proving nothing. Run the gate question before you write the test; if the honest answer is the wrong side of it, stop and rework, because the test is about to verify the wrong thing.
| Anti-pattern | Why it is wrong | Gate question |
|---|
| Testing mock behavior | A *-mock assertion proves the mock is present, not that the real code works; it fails only when the mock is removed | Am I asserting on real behavior or on the mock's existence? |
| Test-only methods in production | A method called only from tests pollutes the production API and is dangerous if real code ever calls it | Is this method used anywhere outside tests? |
| Mocking without understanding | Stubbing a method whose real side effect the test depends on makes the test pass for the wrong reason or fail mysteriously | What side effects does the real method have, and does this test depend on any of them? |
| Incomplete mocks | A mock with only the fields you happened to read hides the rest of the real shape; downstream code reading an omitted field fails silently while the test stays green | Does this mock mirror every field the real response returns? |
| Interaction assertions | Asserting which methods were called couples the test to internals, so it breaks on refactors that change nothing observable | Am I asserting an observable outcome (state) rather than a call sequence? |
When adding any mock, test double, or test utility, read references/test-quality.md first; it holds the worked examples, fixes, and the full gate functions behind this table.
Common Rationalizations
| Excuse | Reality |
|---|
| "Too simple to test" | Simple code breaks. The test takes 30 seconds and documents intent. |
| "I'll write tests after" | Tests that pass immediately prove nothing; you verify what you built, not what was required. |
| "Tests-after achieve the same goals" | Tests-after answer "what does this do?"; tests-first answer "what should this do?" |
| "I already manually tested it" | Ad-hoc, no record, cannot re-run. Tomorrow's change breaks it silently. |
| "Deleting X hours of work is wasteful" | Sunk cost. Keeping unverified code is the real debt; you cannot trust it. |
| "Keep it as reference while I write tests" | You will adapt it. That is testing after. Delete means delete. |
| "I need to explore first" | Fine. Throw the exploration away and start the real implementation with TDD. |
| "The test is hard to write" | Hard to test means hard to use. Listen to the test; simplify the design. |
| "TDD will slow me down" | Debugging in production is slower. The test finds the bug before commit. |
| "It's about the spirit, not the ritual" | The ritual is the spirit. Watching the test fail is the only proof it tests anything. |
| "The behavior is already discovered; rebuilding just risks new bugs" | The understanding survives deletion and makes the rebuild fast. Where the rebuild diverges from the deleted code is exactly where the deleted code could not be trusted. |
| "Just this once" | Every exception becomes the norm under the next deadline. |
| "Production is down, no time for a test" | The reproduction test takes minutes and proves the emergency fix actually fixes the emergency. Shipping an unproven fix to a down system risks a second outage. |
Red Flags: STOP and Start Over
- Code written before its test
- Test passes on its first run
- Cannot explain why the test failed
- "I'll add tests later" or "tests in a follow-up PR"
- "I already manually tested it"
- "Keep it as reference" or "adapt the existing code"
- "Already spent X hours, deleting is wasteful"
- "This case is different because..."
- Mock setup longer than the test logic
- Skipping or disabling tests to make the suite pass
All of these mean: delete the code, restart with TDD.
When Stuck
| Problem | Solution |
|---|
| Do not know how to test it | Write the wished-for API call and the assertion first; ask the user if still stuck |
| Test too complicated | The design is too complicated; simplify the interface |
| Must mock everything | Code too coupled; inject dependencies |
| Test setup is huge | Extract helpers; if still complex, the design needs work |
Verification Checklist
Before declaring the work complete:
Cannot check every box? TDD was skipped somewhere; the unchecked box names where to restart.
After Green: Validate Live
Green tests prove the logic; they do not prove the running system. For a change with a runnable surface, the suite exercises code paths while the rendered UI, the live endpoint, and the real serializer go unobserved. Once the cycle is green, hand off to live validation, the step that follows red, green, refactor:
- Web UI:
validate-web drives a real browser against the dev server.
- HTTP API:
validate-api sends real requests against the running service.
Validate the complete user journey, not just the one behavior you changed: walk the full multi-step flow a real user takes to reach the goal the change serves (arrive, move through each step, act, see the end state), in realistic order, because a change that passes in isolation can still break the flow it lives inside, and the experience the user actually has is the sum of those steps in sequence. This is also the bar for calling the work done: for a user-facing surface, "done" means the full flow was walked and observed working, not just a green suite.
A pure library or a refactor with no behavioral surface has nothing to validate live; say so rather than inventing a step.
Example: Bug Fix
Bug report: empty email accepted by the signup form.
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
$ npm test signup.test.ts
FAIL: expected 'Email required', received undefined
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
}
$ npm test signup.test.ts
PASS
$ npm test
PASS (all)
REFACTOR: extract field validation only if duplication actually exists, with tests staying green.