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.
What the Cursor TDD loop gives you
Section titled “What the Cursor TDD loop gives you”- 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 diffagainst a tagged red commit, plusgit 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.
-
Have the agent write the tests and a stub, nothing else. A stub that throws
Not implementedkeeps the suite red for the right reason and fixes the function signature before any logic exists. -
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.
-
Have the agent implement until green, with the tests read-only. The agent runs the suite in its terminal, reads the failures and iterates.
-
Prove the tests were not touched, then prove they are strong enough. Run the locked-test check and a scoped mutation run.
-
Run the pre-PR gate and open the pull request. CI re-runs the same checks, so nothing depends on your machine.
Write the specification as failing tests
Section titled “Write the specification as failing tests”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, in the repo rootgit add src/lib/markdowngit commit -m "test: markdownToHtml specification (red)"git tag tdd-redThe 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.
Implement until the suite passes
Section titled “Implement until the suite passes”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.
Prove the agent did not touch the tests
Section titled “Prove the agent did not touch the tests”Do not trust the agent’s summary that it left the tests alone. Check it:
# Terminal: exits 1 and prints the diff if any tracked test file differs from the tagged contractgit diff --exit-code tdd-red -- src/lib/markdown/__tests__/# Must print nothing: lists new untracked or uncommitted files under the test directorygit 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.
Prove the tests can catch a bug
Section titled “Prove the tests can catch a bug”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: Stryker 10.0.0 with its Vitest runner (npm, checked 2026-09-26)npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runnernpx stryker run --testRunner vitest --mutate src/lib/markdown/markdown-to-html.ts --incrementalScope --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.
Run the pre-PR gate
Section titled “Run the pre-PR gate”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.
Who signs off, and what do they read?
Section titled “Who signs off, and what do they read?”The verification is split so that no human reads every implementation line, and nobody is asked to trust the agent’s own report.
| Check | Proves | Who owns it |
|---|---|---|
| Test file review before the red commit | The contract says what you want | The developer who asked for the change |
git diff --exit-code tdd-red and git status --porcelain on the test paths | The agent did not rewrite the contract | Developer locally, CI on the pull request |
| Mutation score on changed files | The contract can detect a wrong implementation | Developer; the tech lead sets the bar |
| Types, lint, tests, coverage thresholds | The change is consistent with the rest of the codebase | CI, as a required status check |
| Pull request review | Test diff and public API changes are intended | A 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 well | Works poorly | Use instead |
|---|---|---|
| Parsers, formatters, validators, pricing rules | Visual layout (“does it look right”) | Screenshot or browser checks; see testing patterns in Cursor |
| API handlers with a request and response contract | Orchestration where setup outweighs the code | Integration tests against real services |
| Bug fixes you can reproduce with one input | Performance work | Benchmarks with a budget; correctness tests alone pass slow code |
| Pure data transformations | Legacy code with no tests at all | Characterization 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, repo root: installs into .agents/skills/ for Cursor (skills CLI 1.7.0)npx skills add mattpocock/skills --skill tdd -a cursorThen 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.
Where to go next after TDD in Cursor
Section titled “Where to go next after TDD in Cursor”Using Codex instead? Its version of this workflow is testing with Codex.