---
name: test-driven-development
description: Write a failing test before any implementation code, for every feature and bugfix. Use when implementing new behavior, fixing bugs, or refactoring. Not for root-cause hunting on a mystery failure: use systematic-debugging first. Not for the final done-claim: use verification-before-completion. Exceptions (throwaway prototypes, generated code, config) need explicit human sign-off.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: rewritten from patterns in obra/superpowers (MIT)
  category: Methodology & Process
---

# Test-Driven Development

Write the test first. Watch it fail. Write the minimal code that passes. If you never saw the test fail, you do not know it tests anything.

## The iron law

No production code without a failing test first. Wrote code before the test? Delete it and start over. Not "keep it as reference", not "adapt it while writing tests". Delete. The rewrite is fast because you now know the shape; the confidence is the part you cannot get any other way.

## The cycle

1. **RED: write one failing test.** One behavior, clear name, real code paths (mocks only when unavoidable).
2. **Verify RED: run it and watch it fail.** Mandatory, never skipped.
   - Fails for the expected reason (feature missing): proceed.
   - Passes: you are testing existing behavior. Fix the test.
   - Errors (typo, bad import): fix and re-run until it fails correctly.
3. **GREEN: write the minimal code that passes.** No extra options, no speculative parameters, no refactoring other code.
4. **Verify GREEN: run it and watch it pass.** The rest of the suite stays green and the output stays clean, no new warnings. Test fails: fix the code, not the test.
5. **REFACTOR: clean up while green.** Remove duplication, improve names, extract helpers. No new behavior.
6. Repeat for the next behavior.

## Good vs bad

**Good test, then minimal code:**

```typescript
test('retries failed operations 3 times', async () => {
  let attempts = 0;
  const op = () => { attempts++; if (attempts < 3) throw new Error('fail'); return 'ok'; };
  expect(await retryOperation(op)).toBe('ok');
  expect(attempts).toBe(3);
});
```

Clear name, real behavior, one thing. The passing implementation is a plain three-iteration loop, nothing more.

**Bad:** `test('retry works')` built on a chain of `mockRejectedValueOnce` calls asserting the mock was called three times. It tests the mock, not the code. And the matching bad implementation adds `maxRetries`, `backoff`, and an `onRetry` callback no test asked for.

## Why order matters

Tests written after code pass immediately, and passing immediately proves nothing: wrong thing tested, implementation tested instead of behavior, forgotten edge cases silently blessed. Tests-first answer "what should this do?"; tests-after answer "what did I build?" and inherit its blind spots. Manual testing does not substitute either: no record, no re-run, no proof.

## Bug fixes

A bug is a missing test. Write the failing test that reproduces it, then fix. The test proves the fix and blocks the regression. Never fix a bug without one.

## When stuck

| Problem | Move |
|---|---|
| Don't know how to test it | Write the assertion first against the API you wish existed |
| Test needs huge setup | Design is too coupled. Extract, inject dependencies |
| Everything needs mocks | Same. Listen to the test; hard to test means hard to use |
| Need to explore first | Fine. Spike, throw the spike away, restart with TDD |

Read references/testing-anti-patterns.md before adding mocks or test-only helpers.

## Verification

Pick any new function and run its test with the implementation stubbed out or reverted (`git stash` works). Expect: the test fails for the behavioral reason, not an import error. Restore and run again. Expect: pass, full suite green, output clean. If the test cannot be made to fail by removing the feature, it tests nothing; rewrite it.

## Completion checklist

- [ ] Every new function or changed behavior has a test
- [ ] Watched each test fail before implementing
- [ ] Each failure was for the expected reason
- [ ] Implementation is minimal, nothing beyond the tests
- [ ] Full suite passes, output clean
- [ ] Edge cases and error paths covered
- [ ] No mocks where real code was usable

Any box unchecked: not done. Fix or say so.

## Footguns

- **The test that passes on arrival.** It never proved anything. Break the implementation deliberately; if the test still passes, delete and rewrite it.
- **Sunk-cost code.** "Deleting three hours of work is wasteful" is the trap. The time is spent either way; the choice is between confident code and coverage theater on top of unverified code.
- **Compound tests.** A name containing "and" is two tests. Split them so a failure points at one behavior.

## Red flags

Any of these phrases means stop, delete the untested code, restart with RED:

- "Too simple to test"
- "I'll add tests after"
- "I already manually tested it"
- "Keep it as reference while I write tests"
- "Tests-after achieve the same thing, it's about spirit not ritual"
- "TDD is dogmatic, I'm being pragmatic"
- "This case is different because..."

The rule closes its own loopholes: violating the letter is violating the spirit. If production code exists and no test failed first, it is not TDD, whatever the justification.
