| name | tdd |
| description | Test-driven development (TDD) process used when writing code. Use whenever you are adding any new code, unless the user explicitly asks to skip TDD or the code is exploratory/spike. |
Test-Driven Development Process
TDD is a design technique that uses tests as a tool. Design emerges from usage, not speculation. Short feedback loops let you course-correct immediately. The resulting architecture is testable by design, not retrofitted. We are not trying to rush towards a feature completion, it's important that the code is correct and well-designed, it's crucial to be thorough and only add what tests demand.
When starting, announce: "Using TDD skill in mode: [auto|human]"
MODE (user specifies, default: auto)
- auto: DO NOT ask for confirmation or approval. Proceed through all steps without stopping.
- human: wait for confirmation at key points
STARTER_CHARACTER = 🔴 for red test, 🌱 for green, 🌀 when refactoring, always followed by a space
The Cycle
RED → GREEN → REFACTOR. Every test.
Anti-pattern: RED → GREEN → RED → GREEN → ... → REFACTOR at the end.
Core Rules
- ALL code changes follow TDD. Feature requests mid-stream are NOT exceptions. Write test first, then code.
- One test at a time. Write one failing test, make it pass, refactor. Then think about the next test. Never write multiple tests at once.
- The production API emerges from tests. Do not design method signatures, interfaces, or class structures ahead of what a test demands. If no test has required a parameter, don't add it. If no test has required a class, don't create it.
- Predict failures. State what we expect to fail before running tests.
- Two-step red phase:
- First: Make it fail to compile (class/method doesn't exist)
- Second: Make it compile but fail the assertion (return wrong value)
- Minimal code to pass. Just enough to make the test green. If no test requires it, don't write it. When a test verifies a collaboration (A calls B), "pass" means B's contract exists — not that B works internally. Create the signature, return a hardcoded value. B's behavior emerges from B's own tests.
- No comments in production code. Keep it clean unless specifically asked.
- Run all tests every time. Not just the one you're working on.
- Refactor after every green, not at the end.
- Test behavior, not implementation. Check responses or state, not method calls.
- Push back when something seems wrong or unclear.
Test Planning
- Think about what the code should do from the caller's perspective — not how it works internally
- Sketch the first few tests as single-line
[TEST] comments. Start with the simplest case. This is a starting direction, not a comprehensive list.
- Self-check: are you describing what happens, or how it happens? Each test should name a behavior or outcome. If you catch yourself naming error classes, internal functions, database operations, or implementation mechanisms — you're pre-designing the solution. Rewrite the test.
# These presuppose implementation — rewrite them
[TEST] Throws ForbiddenError when user is not owner
[TEST] Calls mapsDao.transferOwnership
[TEST] Emits transfer tracking event
These lock in error types, internal calls, and infrastructure before a single test runs. Describe what the caller observes instead — leave the how to emerge from making tests pass.
- This list will evolve. New tests emerge during implementation. Tests you planned may turn out wrong. That's expected.
- Do not plan all edge cases upfront. Comprehensive coverage (ZOMBIES) happens in Final Evaluation after the core behavior exists.
- If MODE is human, wait for confirmation after test planning
Implementation Phase
One test. One cycle. No exceptions. Write a single failing test, make it pass, refactor. Only then think about the next test. Do not write multiple tests before making them pass — that is upfront design disguised as testing.
- Replace the next [TEST] comment directly with a failing test. No intermediate markers.
- Test should be in format given-when-then (do not add as comments), with empty line separating them
- Think through the expected value BEFORE writing the assertion. Trace the logic step by step.
- Predict what will fail
- Run tests, see compilation error (if testing something new)
- Add minimal code to compile
- Predict assertion failure
- Run tests, see assertion failure
- Add minimal code to pass
- Predict whether the tests will pass and why. Run tests, see green
- Simplify. For each line/expression you just added, ask: "Does a failing test require this?"
- If no test requires it, delete it or if it's necessary, add a test comment to write that test
- Run tests after each simplification
- Repeat until every line is justified by a test
- Refactor. Zoom out — evaluate the effect of your changes on the surrounding code, not just the diff.
- Read back the function/class/module that received the change. Not just the lines you added — the whole unit.
- Answer each explicitly:
- Cohesion: is this unit still doing one thing, or growing responsibilities?
- Duplication: any repeated patterns across this code and the rest of the codebase?
- Naming: do all names still accurately describe what they hold/do after this change?
- Abstraction: is a domain concept hiding behind a conditional or a parameter?
- New abstractions are allowed. New behavior is not.
🌀 Refactoring: [concrete action] then do it, one change at a time, tests after each
- Only after answering all four checks with no actions identified:
🌀 Clean — [one sentence: what you read back and why nothing needs changing]
- Go to step 1 for the next [TEST] comment. Repeat until all planned tests are passing.
Final Evaluation
- Now walk through ZOMBIES for comprehensive coverage:
- Zero/empty cases? One item? Many items? Boundary transitions? Interface clarity? Exceptions/errors? Simple overlooked cases?
- If there are gaps, add
[TEST] comments for the missing cases and run them through the full Implementation Phase (red → green → refactor, one at a time).
- Is anything still hardcoded in the code that shouldn't be? Fix it, analyze test gaps and go back to previous stages if needed.
- Analyze code expressiveness and quality. If there's anything to improve, go to refactoring phase.