TDD Loop skill for Claude Code
See it fail, make it pass, then clean it up. Makes Claude work test-first for real: it writes a test list, runs every new test to confirm it fails on the assertion rather than a typo, writes the least code to pass, and refactors only on green. It picks the next test deliberately, checks coverage on touched files, and sabotages key code paths to prove the tests can actually catch regressions.
- name
tdd-loop - size192 lines · ≈2,200 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Drives feature work and bug fixes through a strict red-green-refactor loop with real test runs at every step, a written test list, and deliberate choice of the next test. Covers watch-mode commands for Vitest, Jest, pytest, Go, and Cargo, fake-it vs. triangulate decisions, and when to stop. Use when the user asks for TDD or test-first development, says "write the test first", wants a feature built test by test, or is adding behavior to code that already has a test suite. Try asking
- Build the discount code feature test-first, one test at a time
- Use TDD to add a parser for these duration strings like 1h30m
- Write a failing test for this bug first, then fix it
Use it when
- You want a new function or module driven out by tests
- A bug fix should start from a reproducing test
- The code already has a fast test runner you can loop on
- You want small, reviewable steps instead of one large change
Skip it when
- The code has no tests and is hard to isolate — use legacy-code-rescue first
- You need to decide what kinds of tests a module deserves — use unit-testing
How to check the result
Every test in the summary was observed failing before it passed, the full suite and type check are green, and deliberately breaking the key logic turns tests red.
The common mistake it prevents
Writing a test that passes on its first run and moving on, which usually means it never exercised the new behavior at all.
Done well means every line of production code exists because a test demanded it, every test was seen failing for the right reason before it passed, and the code was cleaned up while green. A test you never saw fail proves nothing.
Ground rules
- Run the tests for real after every edit. Never claim red or green from reading code.
- One failing test at a time. If two tests are red, you skipped a step; comment one out.
- Red must fail for the expected reason (assertion mismatch, missing function), not a typo, import error, or broken fixture. A wrong-reason red is not a red.
- In green, write the least code that passes. In refactor, change no behavior and add no features. Never refactor while red.
- Tests describe behavior through the public interface. If a test needs a private method, the design is telling you to extract something.
- Commit (or at least checkpoint) on every green. Small greens make reverts cheap.
Setup before the first test
- Find the existing test command and conventions:
package.jsonscripts,pyproject.toml[tool.pytest.ini_options],Makefile, CI workflow files. - Run the whole suite once. If it is already red, stop and report; do not build on a broken baseline.
- Start a fast feedback loop scoped to the file under work:
| Stack | Focused run | Watch mode |
|---|---|---|
| Vitest | npx vitest run src/cart.test.ts -t "applies discount" | npx vitest src/cart.test.ts |
| Jest | npx jest src/cart.test.ts -t "applies discount" | npx jest --watch |
| pytest | pytest tests/test_cart.py -k discount -x | pytest -x --lf after each save, or ptw if installed |
| Go | go test ./cart -run TestApplyDiscount -v | rerun on save via editor task |
| Rust | cargo test apply_discount | cargo watch -x test if cargo-watch is installed |
| RSpec | bundle exec rspec spec/cart_spec.rb:42 | guard if configured |
Keep a run under ~2 seconds. If the loop is slow, scope tighter before writing tests.
The test list
Before writing any code, write a test list in your notes: behaviors, edge cases, and error paths, one line each. It is a scratchpad, not a contract; add items whenever you think of them and cross them off when green.
Test list — cart discount
- [ ] no discount code -> total unchanged
- [ ] 10% code -> total reduced by 10%, rounded to cents (half-even)
- [ ] expired code -> DiscountExpired error, total unchanged
- [ ] code applied twice -> applied once
- [ ] discount larger than total -> total floors at 0
- [ ] (later) percentage + fixed codes stack in defined order
Workflow
- [ ] 1. Pick the next test from the list
- [ ] 2. Write it; run it; watch it fail for the right reason
- [ ] 3. Make it pass with the smallest change; run the suite
- [ ] 4. Refactor while green; run after each micro-step
- [ ] 5. Update the test list; checkpoint; repeat
- [ ] 6. Stop when the list is empty and the design is clean
1. Choose the next test
Pick the test that teaches the most while forcing the smallest code change.
| Situation | Next test |
|---|---|
| Nothing exists yet | Simplest degenerate case: empty input, zero, no-op |
| One case passes via a hard-coded value | A second example that forces generalization (triangulate) |
| Happy path works | Boundaries: off-by-one, empty vs. one vs. many, max limits |
| Core logic stable | Error paths and invalid input |
| Unsure how to start | A test for the outermost behavior, marked pending, then inner tests to get there |
| A test would require 30+ lines of new code | Too big; split it into a smaller intermediate behavior |
Order by the transformation priority idea: prefer tests whose solution moves from constant -> variable -> conditional -> loop, rather than jumping straight to a complex algorithm.
2. Red
- Name the test as a behavior sentence:
rejects expired codes,test_rounds_half_even. - Arrange-act-assert, one behavior per test. Assert on outcomes, not call counts, unless the interaction is the behavior (e.g. “sends exactly one email”).
- Write the assertion first, then the setup it needs.
- Run it. Read the failure message. If it is a compile/import error, add the minimum stub (empty function, class with the method) so the test fails on the assertion.
3. Green
Choose the strategy deliberately:
| Strategy | Use when |
|---|---|
| Fake it (return a constant) | The general solution is not yet obvious; let the next test force it |
| Obvious implementation | You can write the real code in a few lines with high confidence |
| Triangulate | Two or more examples are needed to see the real pattern |
If the “obvious implementation” fails unexpectedly, drop back to fake-it and smaller steps instead of debugging a large change. Run the full file (or suite) on green, not just the single test, to catch collateral breakage.
4. Refactor
- Remove duplication between test data and production code first (the hard-coded constant from fake-it is duplication).
- Rename, extract, inline, and move in small steps, running tests after each.
- Refactor tests too: shared builders or factories for setup, but keep each test’s key inputs visible in the test body.
- Stop refactoring when the code reads clearly; do not design for imagined tests.
5. Checkpoint
git add -A && git commit -m "test(cart): reject expired discount codes"
Or keep a WIP branch and squash later (see commit-craft). Update the test list.
Bug fixes with TDD
- Write a test that reproduces the bug through the public interface. Watch it fail with the same symptom the user reported.
- Fix with the minimal change. Watch it pass.
- Temporarily revert the fix and confirm the test fails again. Restore.
- Add neighbouring cases the bug suggests (same boundary, other inputs).
Test doubles in the loop
- Default to real collaborators for in-process code. Mock only at boundaries you do not own or that are slow or nondeterministic: network, clock, randomness, filesystem outside a temp dir, third-party APIs.
- Inject the clock and random source rather than patching globals.
- Prefer fakes (in-memory repository) over interaction mocks for stateful collaborators; they survive refactors.
- If a test needs more than two mocks, the unit is doing too much; that is design feedback, not a reason to add a third mock.
Gotchas
- Test passes immediately on first run. Either the behavior already exists (delete or keep as a characterization test, but note it) or the test is not exercising what you think. Break the code deliberately to confirm the test can fail.
- Snapshot tests in the loop make red trivially green with
-u. Use explicit assertions while driving design; add snapshots only for large stable output. - Shared mutable fixtures cause tests to pass in isolation and fail together. Build fresh state per test.
- Asserting on implementation details (private fields, exact SQL, internal call order) freezes the design and makes the refactor step painful.
- Async tests that do not await pass vacuously. Make sure the assertion is awaited or the framework fails tests with unhandled rejections.
- Floating-point money. Use integer cents or a decimal type; assert exact values.
- Skipping refactor because “it works” accumulates the mess TDD was supposed to prevent. Budget one refactor pass per green.
When to stop
- Every item on the test list is crossed off or explicitly deferred with a reason.
- Error paths and boundaries have tests, not just the happy path.
- Production code has no branches that no test exercises: check with coverage on the
touched files (
npx vitest run --coverage,pytest --cov=pkg --cov-report=term-missing) and treat uncovered lines as either missing tests or dead code. - Code and tests read cleanly; no leftover fake-it constants.
Verification
- Full suite green, plus lint and type check.
- For the two or three most important tests, sabotage the production code (flip a comparison, return early) and confirm each test goes red. Restore.
- Diff review: every production change maps to a test on the list.
Output
When reporting back, summarize the loop:
**Behavior added:** Discount codes on cart totals.
**Tests (6, all seen red then green):** no code; 10% code; half-even rounding;
expired code; double application; floor at zero.
**Deferred:** stacking of percentage + fixed codes (needs product decision on order).
**Design notes:** Extracted `Money` value type during refactor; Cart no longer does
rounding itself.
**Checks:** `npx vitest run` 214 passed; `tsc --noEmit` clean; coverage on cart/ 100% lines. About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.