Test-Driven Development
The discipline of writing the test before the implementation. The test defines what success looks like and prevents over-engineering. The implementation is then the smallest change that makes the test pass.
This skill is read by test-writer (always) and by backend-dev / frontend-dev when a task's acceptance criteria are well-defined enough to test first.
When to TDD
| Task type |
TDD? |
Rationale |
| Pure logic (calculations, parsing, state machines) |
Yes |
Tests are cheap, behaviour is precise |
| API endpoints with defined request/response |
Yes |
Spec tells you the test |
| Bug fixes |
Yes — always |
Regression test must fail without the fix |
| MediatR command/query handlers |
Yes |
Inputs and outputs are explicit |
| React components with defined props/output |
Yes |
Testing-Library makes this fast |
| Pure UI styling, layout, animation |
No |
Visual regression / Playwright instead |
| Exploratory spikes / prototypes |
No |
Throwaway code; tests slow exploration |
| Generated code (migrations, scaffolds) |
No |
Test the migration, not the generator |
| Pure plumbing (DI registration, wiring) |
No |
Integration tests cover this |
When in doubt, TDD. The cost of writing a test first is low; the cost of over-engineered untested code is high.
The Loop
Red — write a failing test
- Read the acceptance criterion you're implementing. Pick one behaviour.
- Write a test that asserts the behaviour. The test must:
- Reference the function / endpoint / component you're about to write (it doesn't exist yet, or doesn't behave correctly yet)
- Have a clear assertion (one concept per test — see
code-standards)
- Use the project's existing test framework and patterns
- Run the test. Confirm it fails. Read the failure message — it must fail for the right reason (not "import error", "syntax error", "fixture not found"). If it fails for the wrong reason, fix that first.
A test that fails for the wrong reason is not a red test.
Green — make it pass
- Write the smallest code change that makes the test pass.
- No extra features. No "while I'm here" fixes. No defensive code for cases the test doesn't cover.
- Run the test. Confirm it passes.
- Run the full test suite for the affected area. Confirm nothing else broke.
If you can't make it pass with a small change, the test was probably wrong. Stop and rewrite the test before the implementation.
Refactor — clean up
With the test as a safety net:
- Rename anything unclear
- Extract obvious duplication
- Improve naming, ordering, formatting
- Run the tests after every refactor — they must stay green
Refactoring without tests is editing. Refactoring with tests is safe.
Cycle Length
A red-green-refactor cycle should take minutes, not hours.
- If a single test takes more than 30 minutes to make pass, the test is probably testing too much. Split it.
- If you've been writing implementation for an hour without seeing green, you've left the loop. Stop, get back to a passing state (revert if needed), and write a smaller test.
Small cycles compound. Long cycles produce untested code with the test added at the end as theatre.
Bug Fixes (Mandatory TDD)
For any bug fix:
- Write a regression test that reproduces the bug
- Run it. Confirm it fails — and fails with the same symptom the user reported
- Apply the fix from the
diagnose skill
- Run the regression test. Confirm it passes.
- Run the full suite. Confirm no other test broke.
The regression test is the artifact that prevents the bug from recurring. A bug fix without one is incomplete.
Anti-Patterns
| Anti-pattern |
Better |
| Writing 10 tests, then 10 implementations |
One test, one implementation, one cycle |
| Asserting "no exception thrown" with no behaviour check |
Assert the actual outcome (return value, side effect, state) |
| Testing the test framework or language internals |
Test your code's behaviour |
| Mocking everything until the test asserts nothing real |
Mock external boundaries only (DB, HTTP, time); use the real thing for internal collaborators |
| Writing the test after the implementation, then claiming TDD |
Order matters. The test must fail before the code is written. |
| Skipping refactor because "tests pass, ship it" |
Tests are the licence to refactor; using them to avoid refactoring is waste |
| TDD-ing UI styling |
Use visual regression / Playwright; TDD doesn't fit pixel-level work |
Integration with Sprint Workflow
Phase 1: Implementation
When acceptance criteria are testable and the task is on the TDD list above:
- The implementing agent (
backend-dev / frontend-dev) writes the test first, then the implementation
- The agent commits red and green together (or as a single logical unit) — not in separate commits, since the red test is incomplete
Phase 2: Test Writer
test-writer augments TDD tests with:
- Edge cases not covered by the AC tests (empty collections, boundary values, error paths)
- Integration tests across module boundaries
- Snapshot tests where output format is a contract
test-writer does not duplicate the AC tests — it fills the gaps.
Phase 3: QA
qa-agent (or Codex adversarial review) verifies:
- Every acceptance criterion has at least one test asserting it
- Bug fixes have regression tests
- Tests fail without the implementation (spot-check by reverting one change and running the test)
A task that passes all builds but has no test for its AC fails QA.
Per-Stack Notes
The TDD loop is the same. The mechanics differ:
- .NET (xUnit):
[Fact]/[Theory], in-memory DbContext, Moq for boundaries. See dotnet-api.
- React (Vitest):
@testing-library/react, userEvent for interactions, vi.fn() for mocks. See react-typescript.
- Rust:
#[test] for unit, tests/ for integration, insta for snapshots, tempdir for filesystem. See rust-testing.
- API endpoints: contract test via the response wrapper / status code / RFC 7807 shape. See
api-design.
Read the relevant per-stack skill before writing the first test.
1---2name: tdd3description: Test-driven development loop — red, green, refactor. Use this skill when implementing any feature with clear acceptance criteria, when fixing bugs (regression test first), and inside /sprint-start Phase 1 + Phase 2. Defines cycle length, when NOT to TDD, and integration with sprint quality gates.4---56# Test-Driven Development78The discipline of writing the test **before** the implementation. The test defines what success looks like and prevents over-engineering. The implementation is then the smallest change that makes the test pass.910This skill is read by `test-writer` (always) and by `backend-dev` / `frontend-dev` when a task's acceptance criteria are well-defined enough to test first.1112---1314## When to TDD1516| Task type | TDD? | Rationale |17|---|---|---|18| Pure logic (calculations, parsing, state machines) | **Yes** | Tests are cheap, behaviour is precise |19| API endpoints with defined request/response | **Yes** | Spec tells you the test |20| Bug fixes | **Yes — always** | Regression test must fail without the fix |21| MediatR command/query handlers | **Yes** | Inputs and outputs are explicit |22| React components with defined props/output | **Yes** | Testing-Library makes this fast |23| Pure UI styling, layout, animation | **No** | Visual regression / Playwright instead |24| Exploratory spikes / prototypes | **No** | Throwaway code; tests slow exploration |25| Generated code (migrations, scaffolds) | **No** | Test the migration, not the generator |26| Pure plumbing (DI registration, wiring) | **No** | Integration tests cover this |2728When in doubt, TDD. The cost of writing a test first is low; the cost of over-engineered untested code is high.2930---3132## The Loop3334### Red — write a failing test35361. Read the acceptance criterion you're implementing. Pick **one** behaviour.372. Write a test that asserts the behaviour. The test must:38 - Reference the function / endpoint / component you're about to write (it doesn't exist yet, or doesn't behave correctly yet)39 - Have a clear assertion (one concept per test — see `code-standards`)40 - Use the project's existing test framework and patterns413. Run the test. Confirm it fails. **Read the failure message** — it must fail for the right reason (not "import error", "syntax error", "fixture not found"). If it fails for the wrong reason, fix that first.4243A test that fails for the wrong reason is not a red test.4445### Green — make it pass46471. Write the **smallest** code change that makes the test pass.482. No extra features. No "while I'm here" fixes. No defensive code for cases the test doesn't cover.493. Run the test. Confirm it passes.504. Run the full test suite for the affected area. Confirm nothing else broke.5152If you can't make it pass with a small change, the test was probably wrong. Stop and rewrite the test before the implementation.5354### Refactor — clean up5556With the test as a safety net:57581. Rename anything unclear592. Extract obvious duplication603. Improve naming, ordering, formatting614. Run the tests after every refactor — they must stay green6263Refactoring without tests is editing. Refactoring with tests is safe.6465---6667## Cycle Length6869A red-green-refactor cycle should take **minutes, not hours**.7071- If a single test takes more than 30 minutes to make pass, the test is probably testing too much. Split it.72- If you've been writing implementation for an hour without seeing green, you've left the loop. Stop, get back to a passing state (revert if needed), and write a smaller test.7374Small cycles compound. Long cycles produce untested code with the test added at the end as theatre.7576---7778## Bug Fixes (Mandatory TDD)7980For any bug fix:81821. Write a regression test that **reproduces the bug**832. Run it. Confirm it fails — and fails with the same symptom the user reported843. Apply the fix from the `diagnose` skill854. Run the regression test. Confirm it passes.865. Run the full suite. Confirm no other test broke.8788The regression test is the artifact that prevents the bug from recurring. A bug fix without one is incomplete.8990---9192## Anti-Patterns9394| Anti-pattern | Better |95|---|---|96| Writing 10 tests, then 10 implementations | One test, one implementation, one cycle |97| Asserting "no exception thrown" with no behaviour check | Assert the actual outcome (return value, side effect, state) |98| Testing the test framework or language internals | Test your code's behaviour |99| Mocking everything until the test asserts nothing real | Mock external boundaries only (DB, HTTP, time); use the real thing for internal collaborators |100| Writing the test after the implementation, then claiming TDD | Order matters. The test must fail before the code is written. |101| Skipping refactor because "tests pass, ship it" | Tests are the licence to refactor; using them to avoid refactoring is waste |102| TDD-ing UI styling | Use visual regression / Playwright; TDD doesn't fit pixel-level work |103104---105106## Integration with Sprint Workflow107108### Phase 1: Implementation109110When acceptance criteria are testable and the task is on the TDD list above:111112- The implementing agent (`backend-dev` / `frontend-dev`) writes the test first, then the implementation113- The agent commits red and green together (or as a single logical unit) — not in separate commits, since the red test is incomplete114115### Phase 2: Test Writer116117`test-writer` augments TDD tests with:118119- Edge cases not covered by the AC tests (empty collections, boundary values, error paths)120- Integration tests across module boundaries121- Snapshot tests where output format is a contract122123`test-writer` does not duplicate the AC tests — it fills the gaps.124125### Phase 3: QA126127`qa-agent` (or Codex adversarial review) verifies:128129- Every acceptance criterion has at least one test asserting it130- Bug fixes have regression tests131- Tests fail without the implementation (spot-check by reverting one change and running the test)132133A task that passes all builds but has no test for its AC fails QA.134135---136137## Per-Stack Notes138139The TDD loop is the same. The mechanics differ:140141- **.NET (xUnit)**: `[Fact]`/`[Theory]`, in-memory `DbContext`, `Moq` for boundaries. See `dotnet-api`.142- **React (Vitest)**: `@testing-library/react`, `userEvent` for interactions, `vi.fn()` for mocks. See `react-typescript`.143- **Rust**: `#[test]` for unit, `tests/` for integration, `insta` for snapshots, `tempdir` for filesystem. See `rust-testing`.144- **API endpoints**: contract test via the response wrapper / status code / RFC 7807 shape. See `api-design`.145146Read the relevant per-stack skill before writing the first test.