| name | tdd-loop |
| description | Use when implementing a feature, fixing a bug, or making failing tests pass — anything whose done-condition is "the tests pass". Fires on "implement X", "add a feature with tests", "make the tests pass", "fix this bug test-first", "red-green-refactor", or any task that ships behavior. The discipline: write the failing test first and watch it fail, make the smallest change that turns it green, refactor only while green — and never weaken an assertion to reach green, because a weakened assertion certifies the bug instead of catching it.
|
| metadata | {"short-description":"Red-green-refactor, one behavior at a time; never weaken an assertion to reach green"} |
tdd-loop
Red, green, refactor — one behavior at a time, with the assertion held fixed.
When to use this skill
- The task's done-condition is "the tests pass", or the task ships behavior that a test can
exercise.
- You were handed a failing test suite and asked to make it green.
- You are fixing a bug that has a reproducible case — the case becomes the failing test.
- The project already has a test runner and a test command, so a red test is observable before
the fix and green after it.
- You are about to write production code for which no test exists yet.
- Not for: tasks with no executable test surface — a written analysis, a data extraction whose
answer is a single value, a document, a diagram, or a one-shot script whose only output is
inspected by eye. Manufacturing a test harness for those spends the turn budget without buying a
- Not for: acting on a live workspace — sending mail, changing sharing or permissions, or deleting
content. A red-green loop applies to code you are writing, not to a running service.
signal.
Procedure
-
Pick the seam. Decide which public boundary the test exercises — the function, class, CLI, or
endpoint a real caller uses, not a private internal. A test bound to internals survives the
refactors it should catch and breaks on the refactors it should ignore. If the task statement
names an interface, that interface is the seam; use it exactly as specified, including the name,
the argument order, and the return shape.
-
RED — write one failing test. One behavior per test, named for the behavior
(test_rejects_negative_quantity, not test_2). Take the expected values from a source
independent of the implementation: a literal worked by hand, an example in the task statement, a
known-good constant. Never compute the expectation the same way the implementation will compute
it — that test proves nothing but that the code agrees with itself.
-
Watch it fail, and read the failure. Run the test the same way the build or grader will run
it. It must fail for the reason you predicted. A test that passes before the implementation
exists, or that fails with an import or syntax error, is not red — fix the test first, then
re-run.
-
GREEN — the smallest change that passes. Write only enough code to turn this one test green.
No speculative parameters, no adjacent cleanup, no "while I'm here". Resist generalising ahead of
a test that demands the generalisation.
-
Watch it pass, then re-run the neighbors. The new test green, plus every other test touching
the same area. A green new test next to a newly-red old test is a regression, not progress.
-
REFACTOR — only on green. Improve names, remove duplication, tighten structure, with the
tests as the invariant: they stay green throughout, and you do not edit a test expectation during
a refactor. If a refactor requires an expectation to change, that is a behavior change — go back
to step 2 and do it deliberately.
-
Repeat. Next behavior, next slice: one test, one implementation, repeat, each cycle informed
by what the last one taught. Do not write all the tests up front and then all the code — that
tests imagined behavior rather than real behavior.
The assertion rule (non-negotiable)
When a test goes red during development there are exactly two honest moves: change the code, or
consciously decide the specified behavior was wrong and change the specification and the test
together, saying so explicitly.
Loosening a tolerance, deleting an assert, widening an expected range, wrapping the call in a
try/except that swallows the failure, or marking the case skipped or flaky just to reach green is
fabricating evidence. A weakened assertion is worse than no test at all: it certifies the bug. If an
assertion is genuinely flaky, diagnose the nondeterminism — seed the randomness, pin the ordering,
freeze the clock, isolate the shared state — rather than sanding the assertion down.
The same rule applies to the code under test: special-casing the exact inputs the test uses is not
an implementation, it is a hardcoded answer that will fail every input the test did not name.
Do not
- Do not write production code before a failing test exists for it.
- Do not skip watching the test fail — an unfailed test may be testing nothing at all.
- Do not delete or rewrite a test you were given in order to make the suite pass. A test you did not
write is a specification; make it pass.
- Do not weaken, delete, skip, or tolerance-inflate an assertion to reach green.
- Do not refactor and change behavior in the same step; the tests can protect only one of those at a
time.
- Do not claim the feature "works" from green tests alone. Green means the behaviors you tested held
at the seam you tested them on. Say exactly that, and name what is still untested.
Worked example
Input: "add quantity validation to add_item — negative quantities must be rejected", in a
project that already has a test suite.
Approach: the seam is add_item, the public function a caller uses, not the internal validator you
are about to write. Write one test named test_rejects_negative_quantity that calls
add_item(sku, -1) and asserts the specific exception, with the expected message taken from the
task statement rather than from the code you are about to write. Run it the way the build runs it and
watch it fail with that assertion — not with an ImportError, which would mean the test is wrong,
not the code. Add the smallest guard in add_item that raises, and nothing else — no validation
framework, no adjacent tidy-up. Re-run the new test and the tests around item handling together. Only
then rename or de-duplicate anything, and only while everything stays green. Next behavior — zero
quantity, or a non-integer — starts a fresh cycle at step 2.
If the new guard makes an older test red, that older test is a specification you just broke: fix the
code. Widening its assertion to accommodate the new guard would certify the break.
References
None. Attribution: the method is adapted from the TDD skill in mattpocock/skills (MIT), re-expressed
in original wording.