Claude Code for Test-Driven Development
Why TDD And AI Coding Assistants Usually Fight Each Other
Most developers who try an AI coding assistant for the first time do the same thing: they describe a feature, let the model write the implementation, then write tests afterward to check that it works. That order of operations is exactly backwards for test-driven development, and it's why a lot of engineers conclude that "AI and TDD don't mix." The model writes code that satisfies its own mental model of the problem, then writes tests that confirm the code it already wrote. Nothing gets falsified. Nothing gets specified. You end up with confident-looking code and tests that mostly assert "this function returns whatever this function returns."
Claude Code changes that calculus in a useful way, but only if you use it deliberately. Because it's an agentic coding tool that can run your test suite, read the failure output, and edit files in a loop, it can actually execute the classic red-green-refactor cycle instead of just talking about it. You write a failing test, Claude Code runs it and sees red, Claude Code writes the minimum implementation to go green, and then a refactor pass cleans up the design while the tests hold everything in place. The loop is the same one Kent Beck described decades ago. What's new is that a capable coding agent can drive the loop autonomously, at speed, without losing track of the specification along the way.
This article walks through how to actually set that up: how to prompt Claude Code so it respects red-green-refactor instead of skipping straight to green, a full worked example with real code, how to use hooks and permissions to make the discipline structural rather than just a suggestion, and where the approach breaks down. If you've been treating your AI assistant as an autocomplete engine that occasionally writes a whole function, this is the mental shift that turns it into a disciplined pair programmer instead.
The Core Loop: Red, Green, Refactor With An Agent In The Driver's Seat
Classic TDD has three phases, and each one maps to something Claude Code is specifically good or bad at doing unsupervised.
Red. You write a test that describes a behavior that doesn't exist yet. It must fail, and it must fail for the right reason — a missing method or a wrong assertion, not a typo in the test file itself. This phase is where Claude Code is most valuable as a *thinking partner* rather than an autocomplete tool, because writing a good failing test requires understanding the requirement precisely enough to state it as an assertion. Vague requirements produce vague tests, and vague tests produce implementations that technically pass but miss the point.
Green. You write the smallest amount of implementation code that makes the test pass. Not the most elegant code, not the most general code — the smallest correct step. This is where Claude Code needs the most restraint, because left alone it tends to over-engineer: adding configuration options, extra validation, and generalized abstractions nobody asked for yet. A tight prompt matters more in this phase than any other.
Refactor. With the safety net of passing tests in place, you improve the internal structure of the code without changing its observable behavior. This is where Claude Code's speed actually pays off, because it can run the full suite after every small structural change and immediately tell you if something broke.
The failure mode to watch for is Claude Code collapsing all three phases into one turn: writing the test and the implementation simultaneously in the same response. That's not wrong in every context, but it defeats the purpose of TDD, which is to let the test *specify* behavior before the implementation exists to bias your thinking. The fix is almost entirely about how you phrase the request.
Writing Prompts That Actually Enforce Red Before Green
The single highest-leverage habit is splitting your request into two explicit turns instead of one. Instead of asking for "a function that validates email addresses with tests," ask for the test first, wait for red, then ask for the implementation.
A prompt that works well as a first turn:
We're doing strict TDD. Write ONE failing test for a function
`normalizeDiscountCode(code: string): string` that:
- trims whitespace
- uppercases the result
- throws a ValidationError if the result is empty after trimming
Do not write the implementation. Do not create the ValidationError
class if it doesn't already exist — the test should fail because
the function itself doesn't exist yet. Run the test after writing
it and show me the failure output.Notice the constraint "do not write the implementation" is stated explicitly, twice, in different ways. Claude Code will happily write both halves if you don't forbid it — it's not being careless, it's just optimizing for "solve the whole problem" unless you tell it the problem is deliberately split into steps. The instruction to run the test and show the failure output matters too: you want to see the *actual* red, including the error message, so you can confirm the test fails for the right reason (a missing function) rather than the wrong one (a syntax error in the test file).
Once you've confirmed red, the second turn should be equally constrained:
Now write the minimum implementation to make this one test pass.
Do not add extra validation, extra parameters, or handle cases the
test doesn't cover. Run the test suite after and confirm all tests
are green, including any pre-existing tests in the file."Minimum implementation" is doing real work in that prompt. Without it, Claude Code will frequently add trimming for tabs and newlines specifically, locale-aware casing, a configurable options object, and a couple of defensive checks against non-string inputs — all reasonable engineering instincts, all premature at this stage of TDD. You want exactly enough code to turn the one red test green, because the next red test is what should drive the next increment of behavior.
A Full Worked Example: Building A Rate Limiter Test-First
Let's walk through a slightly larger example to see the loop in practice — a simple token-bucket rate limiter, built entirely from tests forward. This is a good TDD example because the requirements have edges (what happens at exactly zero tokens, what happens on refill) that are easy to get subtly wrong without a test forcing you to be precise.
Step 1 — Red. The prompt: "Write a failing test for a RateLimiter class with a constructor RateLimiter(maxTokens, refillIntervalMs) and a method tryConsume() that returns true if a token was available and false otherwise. Test only that a fresh limiter with maxTokens=1 allows exactly one call to succeed and the second call immediately after fails. No implementation yet."
Claude Code produces:
// rateLimiter.test.js
const { RateLimiter } = require('./rateLimiter');
test('allows exactly one call when maxTokens is 1', () => {
const limiter = new RateLimiter(1, 1000);
expect(limiter.tryConsume()).toBe(true);
expect(limiter.tryConsume()).toBe(false);
});Running this fails immediately with "Cannot find module './rateLimiter'" — that's the correct red. The test is failing because the code doesn't exist, not because of a logic bug in the test itself.
Step 2 — Green. Minimum implementation:
// rateLimiter.js
class RateLimiter {
constructor(maxTokens, refillIntervalMs) {
this.tokens = maxTokens;
}
tryConsume() {
if (this.tokens > 0) {
this.tokens -= 1;
return true;
}
return false;
}
}
module.exports = { RateLimiter };Notice refillIntervalMs isn't used at all yet. That's correct — nothing in the current test requires refilling, so implementing refill logic now would be speculative. This is exactly the discipline that's hard to get from an unconstrained prompt, and exactly what "minimum implementation" prevents.
Step 3 — Red again. Now we add the behavior the class name promises but doesn't yet have: "Add a failing test that after the interval elapses, a token becomes available again. Use fake timers, don't rely on real sleeps."
test('refills a token after the interval elapses', () => {
jest.useFakeTimers();
const limiter = new RateLimiter(1, 1000);
expect(limiter.tryConsume()).toBe(true);
expect(limiter.tryConsume()).toBe(false);
jest.advanceTimersByTime(1000);
expect(limiter.tryConsume()).toBe(true);
jest.useRealTimers();
});This fails, correctly, because the current implementation never refills.
Step 4 — Green again. Minimum change to satisfy both tests:
class RateLimiter {
constructor(maxTokens, refillIntervalMs) {
this.maxTokens = maxTokens;
this.tokens = maxTokens;
this.refillIntervalMs = refillIntervalMs;
this.lastRefill = Date.now();
}
tryConsume() {
this._refillIfDue();
if (this.tokens > 0) {
this.tokens -= 1;
return true;
}
return false;
}
_refillIfDue() {
const elapsed = Date.now() - this.lastRefill;
if (elapsed >= this.refillIntervalMs) {
this.tokens = this.maxTokens;
this.lastRefill = Date.now();
}
}
}
module.exports = { RateLimiter };Both tests pass now. This implementation is deliberately simple — a single refill that tops the bucket all the way up rather than a proper leaky-bucket accrual — because that's all two tests have demanded so far. If you need proportional refill (partial tokens accruing over time rather than a full reset), that's another red test away, not something to guess at now.
Step 5 — Refactor. With two passing tests as a safety net, ask Claude Code to clean up: extract the refill math into something more testable, rename _refillIfDue if a clearer name exists, and re-run the suite after every change. The refactor step is where you should explicitly ask Claude Code to run tests after each individual edit rather than making five changes and running tests once at the end — a batch of unrelated changes followed by one test run tells you *that* something broke, not *which* change broke it.
Making The Discipline Structural, Not Just A Prompt Habit
Prompts are easy to forget, especially mid-session when you're moving fast. Claude Code supports a few mechanisms that turn "please do TDD" from a request you repeat every time into something closer to a project-level rule.
A CLAUDE.md rule. Put a short, explicit instruction in your project's CLAUDE.md file:
## Testing discipline
This project uses strict TDD. When implementing new behavior:
1. Write one failing test and show the failure output before
writing any implementation.
2. Write the minimum code to pass that one test.
3. Only after tests are green, refactor with tests run after
each change.
Never write implementation and tests in the same response.Because Claude Code reads CLAUDE.md at the start of a session, this becomes ambient context rather than something you retype. It doesn't guarantee compliance on every single turn, but it dramatically raises the odds, and it gives you something concrete to point back to if you catch the model skipping a step ("you just wrote the implementation before the test went red — follow the testing discipline in CLAUDE.md").
A hook that runs your test command automatically. Claude Code supports hooks that fire after file edits, and one practical use is auto-running your test runner after every write to a source file, so you see red or green immediately rather than relying on Claude Code to remember to run it. This closes the loop between "code changed" and "test result observed" without depending on the model's initiative.
Permission scoping for the green phase. If you're being strict about "minimum implementation," it can help to explicitly ask Claude Code to list the files it plans to touch before editing, especially in a codebase where a single feature could plausibly justify touching six files. If the plan includes files that have nothing to do with the current failing test, that's a signal the implementation is scope-creeping beyond what the test demands.
Using Subagents To Separate The Test-Writer From The Implementer
One subtlety worth knowing about is that the same conversational context that wrote a test can bias the implementation that follows it — the model remembers the shape of the solution it was already imagining when it wrote the test, so "minimum implementation" can quietly become "the implementation I had in mind all along." A more rigorous setup uses Claude Code's subagent capability to split roles: one agent invocation writes the failing test based only on the requirement, a separate agent invocation (with fresh context, seeing only the test file and the failure output, not the discussion that produced the test) writes the implementation.
This mirrors a practice some teams use with human pairs — one person writes the test, hands it off, and the other implements against it without discussing intent beyond what the test itself communicates. It's slower than doing everything in one continuous thread, and for small changes it's overkill. But for a gnarly piece of business logic where you don't fully trust your own specification yet, forcing that handoff surfaces ambiguity in the test itself: if the implementer-agent has to ask "wait, what should happen when the input is empty," that's the test telling you it under-specified a case, which is exactly the kind of signal TDD is supposed to produce.
Common Failure Patterns And How To Catch Them
The test that can't fail. Watch for tests that would pass even against an empty stub — for example, asserting that a function "doesn't throw" without checking a return value. Claude Code will sometimes write these when it's uncertain about exact expected output. Always read the assertion, not just the test name, before accepting red as legitimate.
Green by cheating. Occasionally the fastest way to satisfy a specific test literally is to hardcode the expected value rather than implement general logic — for instance, an early test with a single input/output pair can be satisfied by a function that just returns that literal value. This is actually a legitimate, if extreme, TDD move (it's sometimes called "faking it"), but it only works as a technique if you follow it immediately with a second test that forces generalization. If you write one test, get a hardcoded green, and stop there, you don't have an implementation — you have a very expensive constant.
Refactor phase silently changing behavior. Ask Claude Code to run the full suite, not a subset, after every refactor step, and to flag immediately if any previously-passing test starts failing, rather than "fixing" the test to match new behavior. A refactor that requires changing a test's expected value isn't a refactor — it's a behavior change wearing a refactor's name tag, and it should go through its own red-green cycle if it's intentional.
Skipping refactor entirely. Because green feels like completion, it's tempting to stop there. Explicitly prompt for the refactor step as its own turn: "Tests are green. Now look at RateLimiter for anything worth cleaning up — naming, duplication, or structure — and run the tests after each change you make." Treating refactor as an optional nice-to-have is how codebases built with AI assistance accumulate the same kind of cruft as codebases built without it; the tests passing was never the point, the tests passing *while the code stays clean* was.
Where This Approach Breaks Down
TDD with an agentic assistant is not a universal solvent. It's a poor fit for exploratory work where you don't yet know the shape of the API you want — writing a test first assumes you can state the interface with some confidence, and for genuinely novel design work, sketching the implementation first and extracting tests afterward is sometimes the more honest process. It's also less useful for pure UI work where the meaningful correctness criteria are visual, though snapshot tests and interaction tests still have a place there.
It also doesn't replace integration and end-to-end testing. Red-green-refactor at the unit level tells you individual pieces behave correctly in isolation; it says nothing about whether those pieces integrate correctly with a real database, a real third-party API, or real network latency. Claude Code can help write and run those broader tests too, but the tight loop described in this article is specifically about unit-level TDD, and conflating the two leads to either unit tests trying to do too much or integration tests never getting written because "we already have tests."
Finally, this only pays off if you actually read the test output. Agentic tools can move fast enough that it's tempting to let the loop run unattended across several cycles and just check the final diff. That defeats the entire premise — the value of TDD is in the specification-by-assertion happening at each step, and if you're not reading each red before it goes green, you've reduced Claude Code back to an autocomplete tool that happens to also run a test command in between edits.
Getting Started This Week
If you want to try this on a real codebase rather than a toy example, start small and deliberately. Pick one function you were about to write anyway, and instead of describing the whole feature in one prompt, split your very next request into the two-turn pattern from this article: ask for one failing test, read the failure output yourself before responding, then ask for the minimum implementation. Do that for a single afternoon before trying to scale it across a whole feature. The habit of pausing at red — actually reading it, not just glancing past it — is the entire skill, and it's a habit you build through repetition, not through a longer prompt template.
Once the two-turn pattern feels natural, add the CLAUDE.md rule so it survives across sessions, and experiment with the subagent split on anything with edge cases you're not fully confident about. The tools do the typing. The discipline of insisting on red before green is still yours to bring, and it's worth bringing deliberately rather than hoping the model remembers on your behalf.
If you want a structured, hands-on path through Claude Code itself — installation, prompting patterns, hooks, subagents, and workflows like the one in this article — our Claude Code Tutorial for Beginners course walks through all of it with real projects, not just slide decks. It's built for developers who want to go from "I typed a prompt and it worked" to "I understand why it worked and can rely on it," which is exactly the mindset test-driven development rewards.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.
Related reading