Skip to content

Test-driven development in Cursor

Test-driven development in Cursor means the agent writes a failing test suite first, a developer approves it as the specification, and the agent then implements until the suite passes without editing it. A locked-test check, mutation testing and CI gates prove the result, so review focuses on the tests rather than every implementation line.

You need a markdown-to-HTML converter for your CMS. Nested lists, code blocks inside blockquotes and HTML entities mean the agent rarely gets every case right in one pass, yet its first attempt looks plausible. Reading 400 lines of parser code to find the three wrong branches is exactly the job you want to stop doing. This page is for developers who drive Cursor’s agent day to day, and for tech leads who want the same loop to be repeatable across a team.

  • Two prompts that split the work cleanly: one that writes only tests and a stub, and one that implements without being allowed to change them.
  • A two-command check (git diff against a tagged red commit, plus git status) that proves the agent did not weaken the tests to get to green.
  • A mutation-testing step with Stryker that tells you whether the tests you approved can catch a bug at all.
  • A pre-PR gate prompt (types, lint, tests, coverage thresholds) and a bug-to-regression-test prompt.
  • Recovery steps for the five ways this loop fails in practice.

For the principle behind the loop, independent of any tool, read test-driven development with AI assistance. This page is the Cursor-specific procedure.

How does TDD with Cursor’s agent work, step by step?

Section titled “How does TDD with Cursor’s agent work, step by step?”

The loop has one rule that everything else protects: the tests are the contract, and only you change the contract. The agent writes the first draft of the tests, you review and commit them, and from then on the agent works on the implementation only.

  1. Have the agent write the tests and a stub, nothing else. A stub that throws Not implemented keeps the suite red for the right reason and fixes the function signature before any logic exists.

  2. Review the tests yourself, then commit them. This is where your reading time goes. You read 40 short assertions instead of 400 lines of parser. Commit them as a separate “red” commit and tag it, so you have a fixed reference point.

  3. Have the agent implement until green, with the tests read-only. The agent runs the suite in its terminal, reads the failures and iterates.

  4. Prove the tests were not touched, then prove they are strong enough. Run the locked-test check and a scoped mutation run.

  5. Run the pre-PR gate and open the pull request. CI re-runs the same checks, so nothing depends on your machine.

Open the agent in a new chat and paste the prompt below. It names Vitest and a file layout; change those two details to match your project.

Now read the test file. You are checking three things: every behaviour you care about has an exact expected string, the expected strings are what you actually want (the agent may encode a guess you disagree with), and no test asserts something the type system already guarantees. Fix the tests by hand or ask for changes. When the tests say exactly what you want, commit them and tag the commit:

Terminal window
# Terminal, in the repo root
git add src/lib/markdown
git commit -m "test: markdownToHtml specification (red)"
git tag tdd-red

The tdd-red tag marks the approved contract. Every later check compares against the tag, not against HEAD, so a commit made after the red commit cannot hide a weakened test.

Start a fresh chat so the implementation is not anchored on the reasoning that produced the tests. The agent runs npx vitest run in its terminal on every iteration. Whether Cursor asks before each command depends on your workspace’s run mode (Cursor docs, Agent security → Run modes, checked 2026-08-28). If your run mode lets you approve specific commands, approve only the test command rather than switching approval off for everything.

Constraint 6 matters more than it looks. Without a stop condition the agent keeps patching individual cases, and the parser grows a branch per test. Cursor’s /loop skill can also repeat a prompt until an outcome is reached (it shipped in Cursor 3.5, 2026-05-20); if you use it, keep the same stop condition in the prompt.

Do not trust the agent’s summary that it left the tests alone. Check it:

Terminal window
# Terminal: exits 1 and prints the diff if any tracked test file differs from the tagged contract
git diff --exit-code tdd-red -- src/lib/markdown/__tests__/
# Must print nothing: lists new untracked or uncommitted files under the test directory
git status --porcelain src/lib/markdown/__tests__/

git diff against the tag catches a weakened test even if it has since been committed, but it does not see new untracked files; git status --porcelain does. If either command prints anything you did not approve, reject the change, restore the tests with git checkout tdd-red -- src/lib/markdown/__tests__/, delete any new test files it listed, and re-run the implementation prompt. When you approve a test addition (the mutant tests or a regression test below), commit it and move the tag with git tag -f tdd-red, so the approved set becomes the new contract. To make the rule stick for the whole team, add it as a project rule: “When asked to implement against existing tests, never modify files under __tests__/; report a suspected wrong test instead.” A rule is an instruction, not a fence. The enforcing layer is a check the agent cannot edit, such as a Cursor hook that checks edits to test paths and rejects the turn, or a CI job that compares the test diff against the approved commit; protecting the oracle shows both patterns.

Green tests you approved are only as good as their assertions. Mutation testing changes the implementation in small ways (flips a comparison, deletes a line) and checks that at least one test fails for each change. A surviving mutant is a bug your suite would ship.

Terminal window
# Terminal: Stryker 10.0.0 with its Vitest runner (npm, checked 2026-09-26)
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker run --testRunner vitest --mutate src/lib/markdown/markdown-to-html.ts --incremental

Scope --mutate to the files you changed; a whole-repo run is slow and tells you little about this change. --incremental stores results so the next run only re-tests changed code. Then hand the survivors back:

The new tests go in a separate file so you can review them as a unit before they join the locked set: once you approve them, commit the file and run git tag -f tdd-red. How high a score to demand before a loop runs unattended is covered in how strong is your oracle.

Turn every bug report into a failing test first

Section titled “Turn every bug report into a failing test first”

The same split applies to bug fixes. The regression test is written and confirmed red before anyone looks for the cause, so the fix is proven and stays proven.

After you approve the test, commit it, move the tag with git tag -f tdd-red, and run the implement-to-green prompt again. For intermittent failures and bugs you cannot yet reproduce, use the investigation workflow in debugging in Cursor first.

Fast checks run first so a type error does not cost a full coverage run. Line coverage alone is a weak signal, so the gate also sets a branch threshold.

The coverage step needs @vitest/coverage-v8 installed (5.0.2 on npm, checked 2026-09-26, matching Vitest 5.0.2). The coverage run also runs every test, so a separate plain vitest run would only repeat it. CI must run the same three commands; the agent’s local run is a convenience, and CI/CD with Cursor shows how to wire them into a workflow.

The verification is split so that no human reads every implementation line, and nobody is asked to trust the agent’s own report.

CheckProvesWho owns it
Test file review before the red commitThe contract says what you wantThe developer who asked for the change
git diff --exit-code tdd-red and git status --porcelain on the test pathsThe agent did not rewrite the contractDeveloper locally, CI on the pull request
Mutation score on changed filesThe contract can detect a wrong implementationDeveloper; the tech lead sets the bar
Types, lint, tests, coverage thresholdsThe change is consistent with the rest of the codebaseCI, as a required status check
Pull request reviewTest diff and public API changes are intendedA reviewer, reading tests first; code review in Cursor covers Bugbot as a second reader

The reviewer reads the test diff line by line and the implementation diff by exception: where mutation survivors were marked equivalent, where coverage is below threshold, and where the public signature changed.

When is TDD with Cursor’s agent the wrong tool?

Section titled “When is TDD with Cursor’s agent the wrong tool?”
Works wellWorks poorlyUse instead
Parsers, formatters, validators, pricing rulesVisual layout (“does it look right”)Screenshot or browser checks; see testing patterns in Cursor
API handlers with a request and response contractOrchestration where setup outweighs the codeIntegration tests against real services
Bug fixes you can reproduce with one inputPerformance workBenchmarks with a budget; correctness tests alone pass slow code
Pure data transformationsLegacy code with no tests at allCharacterization tests first, then TDD for the change

For code with rich input spaces, example-based tests invite the agent to fit the examples. Invariants checked by a property-based test (for example with fast-check 4.10.2) are harder to overfit.

Add a TDD skill so you do not repeat the prompt

Section titled “Add a TDD skill so you do not repeat the prompt”

If you run this loop daily, a skill can carry the red-green-refactor discipline instead of a pasted prompt. The tdd skill from Matt Pocock’s mattpocock/skills repository is the most-installed TDD skill on skills.sh, with 966,524 installs in a 2026-09-26 snapshot (SECONDARY: read through a search snapshot, because skills.sh was not reachable directly).

Terminal window
# Terminal, repo root: installs into .agents/skills/ for Cursor (skills CLI 1.7.0)
npx skills add mattpocock/skills --skill tdd -a cursor

Then ask the agent: “Fix the rounding bug in invoice totals test-first.” The skill has it write a failing test, run it red, make the minimal fix, confirm the test passes and refactor. A skill is third-party instructions that run with your permissions, so read its SKILL.md before installing (skill security). A skill does not replace the locked-test check above: it tells the agent to behave, it does not prove that it did. More options, including Superpowers’ test-driven-development, are compared in the most-installed practice skills.

What breaks when Cursor’s agent runs the TDD loop?

Section titled “What breaks when Cursor’s agent runs the TDD loop?”

The agent edits the tests to get to green. This is the most damaging failure because the result looks like success. Symptoms: a lower test count, a loosened toBe turned into toContain, an added .skip. Recovery: the git diff --exit-code check catches it; restore the tests from the tdd-red tag, start a new chat and re-run the implementation prompt. If it recurs, move enforcement out of the prompt into a hook or CI check.

The agent special-cases test inputs. The implementation contains a branch that matches a literal string from a test. Mutation testing and code review both surface it, and a handful of unseen inputs is a fast detector: ask the agent for ten new markdown samples, have it predict the HTML, then run them. Recovery: add property-based tests or a holdout set of examples the agent never sees.

The tests pass for any implementation. Assertions such as toBeDefined() or typeof result === 'string' cannot fail. Recovery: the tests-first prompt forbids them; the Stryker run exposes the survivors they let through.

The tests are coupled to internals. Tests that spy on private helpers break on every refactor and push the agent to preserve the wrong structure. Recovery: rewrite them against the public function only, and keep “test public behaviour only” in your project rule.

The loop stalls. The failure count stops falling after three or four iterations, usually because the remaining tests expose a design flaw (a line-by-line parser cannot handle nesting). Recovery: stop the chat, switch to Plan Mode and ask for an analysis of the failing tests and a proposal for a different design before any more code is written. Approve the plan, then run the implementation prompt again in a new chat.

Using Codex instead? Its version of this workflow is testing with Codex.