# Test-driven development in Cursor

URL: https://developertoolkit.ai/en/cursor-ide/test/testing-tdd/
Description: Run TDD with Cursor's agent: you approve failing tests as the spec, the agent implements until green without touching them, and mutation tests prove the suite.

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

- 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](/en/method/principles/test-driven-development/). This page is the Cursor-specific procedure.

## 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.

### 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.

**Copy-paste prompt: tests first, no implementation**

```text
Write a test suite at src/lib/markdown/__tests__/markdown-to-html.test.ts for a
function markdownToHtml(input: string): string. Do NOT implement it.

Cover these behaviours, one describe block each:
1. Headings h1-h6, including inline formatting inside a heading
2. Paragraphs separated by one or more blank lines
3. Emphasis: *italic*, **bold**, ***bold italic***, ~~strikethrough~~
4. Links [text](url) and [text](url "title"); images ![alt](src "title")
5. Inline code, fenced code blocks with a language hint, indented code blocks
6. Unordered (-, *, +) and ordered lists, nested three levels, mixed types
7. Blockquotes: single, nested, and containing lists or code
8. Escaping: &, <, >, " in text become entities; markdown inside code is NOT parsed
9. Edge cases: empty string, whitespace only, \r\n line endings, unclosed ** at end of input

Rules for the assertions:
- Assert the exact output string with toBe(), never toBeDefined(), toBeTruthy() or typeof checks.
- Test only the public function. Do not mock or spy on internal helpers.
- One behaviour per test, with a name that states the expected output.

Also create src/lib/markdown/markdown-to-html.ts exporting the signature and throwing
new Error("Not implemented"). Run `npx vitest run src/lib/markdown` and show me the
failure count. Every test should fail with "Not implemented"; if any test passes,
tell me which one and why.
```

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:

```bash
# 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.

### 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 run mode, which you set in Settings > Agents > Approvals & Execution: Auto-review (the recommended default since Cursor 3.6), Allowlist or Run Everything (Cursor docs, Agent security → Run modes, checked 2026-10-12). To let the test command run unasked, add only that command to your allowlist rather than switching approval off for everything with Run Everything.

**Copy-paste prompt: implement to green, tests are read-only**

```text
Implement markdownToHtml in src/lib/markdown/markdown-to-html.ts so that every test in
src/lib/markdown/__tests__/markdown-to-html.test.ts passes.

Constraints:
1. The test file is read-only. Do not edit, skip, rename or delete any test, and do not
   add .skip, .only or .todo anywhere.
2. Do not special-case test inputs (no branches that match a literal string from a test).
3. No markdown parsing library; write the converter from scratch.
4. After each change, run: npx vitest run src/lib/markdown
5. If a test looks wrong, stop and tell me which test and why. Do not work around it.
6. If the failure count has not dropped for three iterations in a row, stop and
   summarise the failing tests and your hypothesis instead of trying again.

When all tests pass, run npx tsc --noEmit and report the final test count.
```

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

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

```bash
# 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](/en/cursor-ide/quick-start/project-rules/): "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](/en/workflows/test/protect-the-oracle/) shows both patterns.

### 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.

```bash
# 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:

**Copy-paste prompt: kill surviving mutants with new tests**

```text
Read the Stryker report in reports/mutation/ for src/lib/markdown/markdown-to-html.ts.
For each surviving mutant, add ONE test to a NEW file
src/lib/markdown/__tests__/markdown-to-html.mutants.test.ts that fails on the mutant
and passes on the current code. Do not change markdown-to-html.ts or the existing
test file. Re-run the same stryker command and report the mutation score before
and after, plus any mutant you think is equivalent (cannot change behaviour) and why.
```

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](/en/workflows/test/oracle-strength/).

### 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.

**Copy-paste prompt: bug report to regression test**

```text
Bug: pasting markdown with Windows line endings (\r\n) produces an empty <p></p>
between every paragraph.

1. Add a test to src/lib/markdown/__tests__/markdown-to-html.test.ts that reproduces
   this with the smallest possible input and asserts the correct HTML.
2. Run it and show me that it fails for the reason described, not for another reason.
3. Stop and wait for my approval of the test.
```

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](/en/cursor-ide/test/bug-hunting/) first.

### 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.

**Copy-paste prompt: pre-PR gate**

```text
Run these checks in order. On a failure, fix the cause in the implementation and
re-run that check before moving on. Never edit a test file to make a check pass.

1. npx tsc --noEmit
2. npx eslint .
3. npx vitest run --coverage --coverage.thresholds.lines=80 --coverage.thresholds.branches=75

Stop when all three pass in one clean sequence. Then list every file you changed,
confirm that `git diff --exit-code tdd-red -- src/lib/markdown/__tests__/` exits 0,
and confirm that `git status --porcelain src/lib/markdown/__tests__/` prints nothing.
```

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](/en/cursor-ide/ship/ci-cd/) shows how to wire them into a workflow.

## 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](/en/cursor-ide/ship/code-review/) 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?

| 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](/en/cursor-ide/test/testing-patterns/) |
| 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](/en/workflows/test/property-based-testing/) (for example with fast-check 4.10.2) are harder to overfit.

## 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).

```bash
# 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](/en/ecosystem/skills/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](/en/ecosystem/skills/top-practice-skills/).

## 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

- [Testing patterns in Cursor](/en/cursor-ide/test/testing-patterns/) — Tests for existing code, growing a suite, fixtures and data factories.
- [Protecting the oracle](/en/workflows/test/protect-the-oracle/) — Hooks, CODEOWNERS and CI checks the agent cannot edit.
- [How strong is your oracle?](/en/workflows/test/oracle-strength/) — Mutation score and the bar a loop must clear to run unattended.
- [The same loop in Claude Code](/en/claude-code/test/testing/) — Test-first workflows in the terminal with Claude Code.

Using Codex instead? Its version of this workflow is [testing with Codex](/en/codex/test/testing/).
