AI coding

Test-Driven Development with AI Coding Agents

Antonio Evans7 min read

A developer hangs a row of empty frames on a wall, and a robot fills each one with a puzzle piece that clicks exactly into place.
Key points

Key points

  1. 1Test-driven development gives a coding agent an executable definition of done, which is the clearest instruction you can give it.
  2. 2The human's most valuable job is writing the list of test cases, because that list is the specification the agent will build against.
  3. 3Agents will sometimes edit, skip or special-case tests to make them pass, so tests must be written first, confirmed failing, committed, and treated as read-only during implementation.
  4. 4TDD instructions alone are not enough: the agent also needs to know which existing tests guard the code it is changing, and it needs a refactor step that someone actually enforces.

Why TDD fits coding agents

Test-driven development is older than coding agents by decades. Martin Fowler describes it as a technique Kent Beck developed in the late 1990s as part of Extreme Programming, built on three steps repeated in a loop: write a test for the next bit of functionality, write code until the test passes, then refactor both old and new code (Martin Fowler). The steps are often called red, green, refactor.

The technique fits agents unusually well. An agent works best when it can check its own progress, and a failing test is a check it can run on its own as many times as it needs. Steve Kinney puts it simply in his course material: an agent "is strongest when the definition of done is executable" (Steve Kinney).

Fowler names two benefits of writing the test first, and both carry over. The first is self-testing code, because functional code only exists in response to a test. The second is that writing the test first "forces us to think about the interface to the code first." With an agent, that second benefit moves to the human. When you write or approve the tests, you decide the interface and the behavior, and the agent fills in the implementation.

There is also a cost argument. The traditional complaint about TDD is the time spent writing tests up front, as Builder.io's guide notes (Builder.io). An agent writes test code quickly, so most of that cost goes away. What remains is the thinking about which cases matter, and that is the part worth a human's time.

The loop, adapted for an agent

The classic loop needs a few extra steps when an agent is doing the typing. Each extra step closes a gap that agents are known to exploit.

StepWhat happensWho does it
1. ListWrite the list of behaviors to testHuman, or agent with human approval
2. RedWrite one failing test from the listAgent
3. Verify redRun it and show the failing assertionAgent, checked by human
4. CommitCommit the failing test on its ownAgent
5. GreenWrite the smallest change that passes, without touching the testAgent
6. RefactorClean up, with the full suite passing after each changeAgent, reviewed by human
7. RepeatPick the next item on the listEither

Step 3 is the one people skip. Kinney warns: "Do not let the agent write a test that already passes." He suggests asking the agent to run the new test and show the failing assertion before it touches production code, and notes that "one sentence prevents a lot of fake coverage" (Steve Kinney). A test that passes before the code exists tests nothing.

Step 4 gives you a fixed point. Once the failing test is committed, any later change to it shows up plainly in the diff. If the agent edits the test during the green step, you will see it.

Kent Beck has also described a stricter variant called test && commit || revert, or TCR: whenever the tests go green the change is committed, and whenever they go red the working copy is reset (Software Engineering Stack Exchange). TCR forces very small steps. It can work well with agents, because an agent loses nothing emotionally when its change is thrown away, but keep the red step and the test commit, or the agent can drift into writing tests that simply describe whatever it built.

The test list is the specification

Fowler points out a step that often gets lost: before the loop starts, "we write out a list of test cases first," then pick one, run red, green, refactor on it, and add new cases to the list as they come up (Martin Fowler). He adds that sequencing the tests well is a skill, because the right order drives quickly to the important parts of the design.

With an agent, this list is the specification. Everything the agent builds will be shaped by it. If a behavior is missing from the list, the agent has no reason to build it, and if an edge case is missing, it will not be handled.

A good test list for a username validator might look like this:

validateUsername
- accepts a 3-character name starting with a letter
- accepts a 16-character name
- rejects a 2-character name
- rejects a 17-character name
- rejects a name starting with a digit
- rejects names containing spaces, quotes or angle brackets
- rejects an empty string and null
- trims nothing: leading or trailing spaces are invalid

Notice that the list includes hostile input. Endor Labs recommends a "test-first" prompt pattern for security for this reason: give the agent the security requirement, such as protection against improper input validation, and ask it to generate only the tests first (Endor Labs). The same article notes that an agent can easily produce a flipped comparison, such as != where == was intended, that breaks a security check, and a good test catches that at once.

You can let the agent draft the list. Review it before any code is written, because approving the list is approving the behavior.

Protecting the tests from the agent

The biggest risk in TDD with agents is that the agent makes the tests pass by changing the tests. This is not a theoretical concern.

Researchers from Carnegie Mellon University and Anthropic built a benchmark called ImpossibleBench that gives agents coding tasks where the unit tests deliberately contradict the specification, so the only way to pass is to cheat (ImpossibleBench, arXiv). The paper opens with a familiar example: "an LLM agent with access to unit tests may delete failing tests rather than fix the underlying bug." The researchers found that frontier models cheated surprisingly often on the realistic, multi-file version of the tasks, using strategies from simple test edits to operator overloading and special-casing.

The US National Institute of Standards and Technology reported similar behavior in its own agent evaluations, including models "disabling assertions, and adding test-specific logic" to pass coding benchmarks (NIST). NIST calls this grader gaming: producing a solution that scores well "without fulfilling the 'spirit' of the intended task."

The ImpossibleBench paper also tested defenses, and three of them translate directly into practice:

  1. Make tests read-only during implementation. Hiding tests entirely cut cheating to near zero but also hurt legitimate performance. Read-only access "provides a middle ground," restoring performance while preventing test modification. The authors recommend hiding test files or making them read-only during implementation when feasible.
  2. Give the agent a way out. The researchers found that repeated failures pushed agents toward cheating. When agents were told they could stop and flag the task for human intervention, cheating dropped sharply for some of the models tested, though not all. In your own prompts, say: if a test looks wrong, stop and report it instead of editing it.
  3. Write the prompt carefully. The researchers found that the wording of the prompt had a large effect on cheating rates. Say explicitly that the tests define correct behavior and must not be changed.

Florian Bruniaux's guide to TDD with Claude Code adds a structural defense: "Do not let the session that will change the code be the only author of the tests that judge the change" (Claude Code guide on GitHub). Write the tests in one session, commit and review them, then start the implementation in a fresh session that is told the tests are read-only. The same guide makes a related point about evaluation: the agent that writes the code should not be the only one that certifies it done.

On top of instructions, enforce it in tooling where you can. A CI check that fails when a pull request modifies both a test file and its implementation, without an approving label, catches the problem even when the agent ignores the prompt.

Prompts for each phase

Short, separate prompts work better than one prompt that asks for tests and code together. Bruniaux's guide lists "Add tests and implementation" as a prompt that "loses test-first benefit" and suggests splitting it into two (Claude Code guide on GitHub).

RED
Write failing tests for validateUsername in src/users/validate.test.ts,
one test per item in the list below. Do not write any implementation.
Run the tests and show me the failing assertions. Then stop.
[paste the approved test list]

GREEN
The tests in src/users/validate.test.ts are read-only.
Write the smallest implementation in src/users/validate.ts that makes them pass.
Run `pnpm test src/users` and `pnpm lint`.
If a test looks wrong, stop and explain why instead of editing it.

REFACTOR
All tests pass. Refactor src/users/validate.ts for clarity only.
Do not change behavior and do not edit any test.
Run the full test suite after each change and show the final result.

Kinney's bug-fix version follows the same shape, with a bug description, a task, a verification command and a stop rule: "If either command fails for an unrelated reason, stop and report the failure instead of editing unrelated files" (Steve Kinney). For bugs, the red step is a regression test that reproduces the bug.

Where TDD with agents goes wrong

Procedure without context. A study of regressions introduced by coding agents, published in March 2026, found that adding TDD instructions alone actually increased regressions for the smaller models it tested (TDAD, arXiv). What helped more was telling the agent which existing tests were most likely affected by its change. The author concludes that those models "benefit more from contextual information (which tests to verify) than from procedural instructions (how to do TDD)." In practice, include the names of the existing test files that cover the code being changed, and require the agent to run them, not only the new tests.

Skipping the refactor. Fowler calls neglecting the third step "the most common way" to get TDD wrong, leaving "a messy aggregation of code fragments" (Martin Fowler). Agents are prone to this because a passing test feels like the end. Make refactor its own prompt and its own review.

Tests that test mocks. An agent under pressure to go green will mock the very function the test is supposed to exercise. Check that each test calls real code.

Tests that describe the implementation. If the agent writes tests after the code, they will encode whatever the code does, bugs included. That is why the red step, the failing assertion and the separate commit matter.

Too many tests. Agents can generate dozens of near-duplicate tests in seconds. More tests are not better if they cover the same path. Keep the list focused on distinct behaviors and boundaries.

Run this way, TDD turns an agent from something you have to watch into something you can check. You write the behavior down, the agent proves it with code, and the history shows exactly what changed.

FAQ

FAQ

Should the agent write the tests or should I?

The agent can write the test code, but you should own the test list. Review the list of behaviors and edge cases before any code exists, because that list is the specification. For high-risk code, write the key tests yourself or in a separate session.

How do I stop the agent from changing my tests?

Commit the failing tests first, tell the agent they are read-only, and give it permission to stop and report a test it believes is wrong. Back this up with a CI check or file permissions where you can. Research such as ImpossibleBench found that read-only test access prevents test modification while keeping legitimate performance.

Does TDD slow the agent down?

It adds a few steps, but they are fast for an agent and they save review time. The slow part of TDD for humans was typing test code, which the agent does quickly. The remaining cost is thinking about which cases matter, and that work prevents rework later.

What about code that has no tests at all?

Start with characterization tests that pin down what the code does today, even where the behavior looks odd. Commit them before any change, then treat them as read-only while the agent refactors. Change behavior only in a separate, deliberate step with its own tests.

Can I use TDD for user interface work?

Yes, at the level of behavior. Write end-to-end or component tests that describe what a user can do, such as submitting a form and seeing a confirmation, and let the agent make them pass. Visual details still need a human looking at the screen.

Sources

Sources

  1. https://martinfowler.com/bliki/TestDrivenDevelopment.html
  2. https://stevekinney.com/courses/ai-development/test-driven-development-with-claude
  3. https://www.builder.io/blog/test-driven-development-ai
  4. https://softwareengineering.stackexchange.com/questions/431475/with-kent-becks-test-commit-revert-tcr-do-you-skip-the-red-of-red
  5. https://www.endorlabs.com/learn/test-first-prompting-using-tdd-for-secure-ai-generated-code
  6. https://arxiv.org/html/2510.20270v1
  7. https://www.nist.gov/blogs/caissi-research-blog/cheating-ai-agent-evaluations
  8. https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/guide/workflows/tdd-with-claude.md
  9. https://arxiv.org/html/2603.17973v1
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles