Skip to content

Testing patterns in Cursor are prompts and checks that make Cursor’s agent write tests from specified behaviour instead of from the code it just read. They cover existing and legacy code, error paths, production bugs, coverage gaps, test data, browser tests and flaky tests. Each pattern ends with evidence that the tests can fail.

You shipped a payment service with no automated tests. You ask Cursor’s agent for a suite, it writes 30 tests, all green, and coverage jumps to 85%. Two weeks later a refactor breaks refunds and nothing goes red, because every assertion restated what the code already did. This page is for the developer who writes the prompts and the tech lead who decides what a test-bearing pull request must prove.

What you’ll walk away with from these Cursor testing patterns

Section titled “What you’ll walk away with from these Cursor testing patterns”
  • A project rule and run-mode setup that lets the agent run tests on its own without running anything destructive
  • Eight copy-paste prompts: behaviour-first suites, async and error paths, characterization tests, bug-to-failing-test, coverage gaps, test data factories, Playwright end-to-end tests, and flaky-test triage
  • A five-item evidence checklist that tells you whether agent-written tests can catch a bug, without reading every test
  • A table of the ways agent-written tests fail and how to recover from each

Pick the pattern by what you know about the correct behaviour. The source of “correct” (the oracle) decides what the tests may assert.

Your situationPatternWhere “correct” comes from
New feature or behaviour changeTest-driven development in CursorYour spec, written before the code
Working code, few or weak tests, known rulesBehaviour-first suiteBusiness rules you state in the prompt
Legacy code nobody fully understandsCharacterization testsCurrent behaviour, recorded before you change it
A bug reached productionBug to failing testThe bug report
Coverage report shows untested branchesCoverage gapsThe rules behind each uncovered branch
A user journey spans pages and servicesPlaywright MCP end-to-end testsAcceptance criteria for the journey
A test fails one run in tenFlaky-test triageThe test’s own intent, made deterministic

Set up Cursor so the agent can run tests safely

Section titled “Set up Cursor so the agent can run tests safely”

Every pattern below depends on the agent running the suite, reading the failure, and trying again. Set this up once per repository.

  1. Let test commands run without a prompt. Cursor decides which terminal commands the agent runs unasked through its run modes (the Run modes page under Agent security in Cursor’s docs, verified 28 August 2026). Cursor’s docs name three modes, Auto-review (the recommended default since Cursor 3.6), Allowlist and Run Everything, under Settings > Agents > Approvals & Execution (checked 2026-10-12). Choose the mode that runs test, type-check and lint commands freely and still asks before anything that deletes data or touches a remote. Do not use Run Everything, which runs every command unattended, outside a disposable, network-restricted sandbox.

  2. Write the testing conventions into a project rule. A rule keeps the conventions in every session, so you stop repeating them in each prompt. See project rules for scopes and file placement.

  3. Commit before the agent starts. A clean commit makes git diff --stat show exactly which files the agent touched, which is how you catch an edited assertion.

  4. Make the test command obvious. Name it in the rule (for example npx vitest run) so the agent does not guess between npm test, jest and a stale script.

A rule is an instruction, not a lock. To make the “do not edit tests” line enforceable, Cursor’s hooks can “observe, block, or modify behavior” at stages of the agent loop (cursor.com/docs/hooks, verified 28 August 2026). Protect the oracle shows the blocking logic and CI checks that sit outside the agent’s reach.

Generate tests for existing code from behaviour, not implementation

Section titled “Generate tests for existing code from behaviour, not implementation”

The common failure is a suite that describes the code rather than the requirement. When the agent reads payment-service.ts and writes tests from it, every test passes on the first run, including on the bugs. State the rules yourself and treat the code as a suspect.

A test that fails here is the most valuable output of the session: either the rule you stated is wrong, or the code has a bug you just found. Decide which before anyone edits anything.

The same prompt shape works at other layers. Name the conventions file so the agent matches your queries and matchers instead of inventing new ones.

Write Vitest + React Testing Library tests for @src/components/UserProfile.tsx.
Cover: renders the name and avatar from props, falls back to initials when
the avatar is missing, calls onEdit once when the edit button is clicked, and
disables the edit button while `saving` is true. Query by role and label,
never by class name. Follow @src/components/__tests__/Card.test.tsx.

Test the async and error paths that coverage hides

Section titled “Test the async and error paths that coverage hides”

A green happy-path test hides the retry, the timeout and the rejected promise. Name each one, and ask for call-count assertions, because “it eventually returned data” does not prove the retry logic ran.

it('does not retry on a 4xx response', async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404 });
vi.stubGlobal('fetch', fetchMock);
await expect(fetchUserData(1)).rejects.toThrow(FetchUserError);
expect(fetchMock).toHaveBeenCalledOnce();
});

The toHaveBeenCalledOnce() line is the one that catches a bug. Without it, a client that retries a 404 three times still passes.

Pin legacy behaviour with characterization tests

Section titled “Pin legacy behaviour with characterization tests”

For code nobody fully understands, you cannot state the rules yet. Characterization tests record what the code does today, including behaviour that looks wrong, so a refactor that changes behaviour turns a test red. They are the prerequisite for a legacy refactor in Cursor; the tool-neutral method is in characterization tests.

Review the SUSPECT: comments with someone who knows the domain. Each one either becomes a real bug ticket or a documented rule. Do not let the agent “fix” them during the refactor that follows.

Turn a production bug into a failing test first

Section titled “Turn a production bug into a failing test first”

A bug fix without a failing test is a guess. Make the agent reproduce the bug as a test, show you the failure, and only then fix it. The full reproduce-diagnose-fix loop is in AI-assisted debugging in Cursor.

The pasted failure from step 1 is the evidence. A regression test that was never seen red proves nothing about the bug.

Close coverage gaps without chasing the percentage

Section titled “Close coverage gaps without chasing the percentage”

A coverage percentage is not a quality measure: 100% line coverage with weak assertions still ships bugs. Use the report to find which branches nobody tests, then ask for tests that would catch a regression on the risky ones.

File | % Stmts | % Branch | % Funcs | % Lines
---------------|---------|----------|---------|--------
auth/login.ts | 75.00 | 66.67 | 100.00 | 75.00
auth/reset.ts | 45.00 | 33.33 | 66.67 | 45.00

In a report like this, auth/reset.ts has the thinnest branch coverage, so the password-reset edge cases (expired token, already-used token, unknown email) are the tests worth writing first. Vitest needs a coverage provider such as @vitest/coverage-v8 installed for --coverage to work. Set coverage.include: ["src/**"] in the Vitest config, or files with no tests at all are missing from the report. To measure whether the tests catch bugs rather than touch lines, see oracle strength.

Inline objects in every test drift from the real types and make each test longer than the behaviour it checks. Ask for factories once and name the file in your testing rule.

Good output looks like this:

let seq = 0;
const nextId = () => ++seq;
export function createUser(overrides: Partial<User> = {}): User {
const id = nextId();
return { id: `user-${id}`, email: `test-${id}@example.com`, name: 'Test User', ...overrides };
}

The type check is the verification here: if User gains a required field, the factory stops compiling instead of silently producing invalid data. For shared fixtures, seeding and anonymised production data, see test data management.

Drive end-to-end tests with the Playwright MCP server

Section titled “Drive end-to-end tests with the Playwright MCP server”

For journeys that cross pages, the Playwright MCP server (@playwright/mcp) lets the agent drive a real browser, so it can write a Playwright test and then run it against your local app. Add it to the project’s .cursor/mcp.json; MCP setup covers the other configuration locations.

{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--isolated"]
}
}
}

--isolated keeps the browser profile in memory. Playwright’s README notes that a persistent profile can only be used by one browser instance at a time, so parallel agents conflict without it. The old @modelcontextprotocol/server-puppeteer package is archived; use the Playwright server. Its tool schemas and page snapshots take context, so enable it for UI work, not for every session.

test('shows an error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('wrong-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toContainText('Invalid credentials');
});

Accessible locators survive the CSS refactors that break #id and .class selectors, and they fail when a control loses its label, which is an accessibility bug worth catching. The committed spec, not the agent’s browser session, is what runs in CI. Broader E2E strategy is in end-to-end test automation.

A test that fails one run in ten wastes more time than one that always fails. The agent’s advantage is that it can run the test many times without getting bored.

For Vitest unit tests, loop in the terminal instead: for i in $(seq 25); do npx vitest run src/cart/__tests__/cart.test.ts || echo FAIL; done. The fix is usually isolation, a teardown hook or an awaited settle, not a bigger timeout. Race conditions that need deeper investigation belong in bug hunting.

How do you verify agent-written tests without reading them all?

Section titled “How do you verify agent-written tests without reading them all?”

Check that the tests can fail and that nobody bent them to fit the code. Ask for this evidence on every pull request that adds tests, and read test code only where an item is missing or surprising.

  1. Red before green. New tests for new behaviour or a bug fail on the old code, and the failure output is in the pull request. Characterization tests are the exception: they pass by design.
  2. Break-it check. For each new test, the agent makes the smallest change that should break it, shows the failure, and reverts. A test that stays green is decoration. Oracle strength scales this into mutation testing across a suite.
  3. Tests untouched during the fix. git diff --stat for the implementation commit lists no test files.
  4. No hollow mocks. The prompt below finds tests that would pass with the implementation deleted.
  5. Gates green in CI, not only in the agent’s report: the test suite, tsc --noEmit and the linter. CI/CD with Cursor wires them in.

The developer who opened the pull request owns this evidence. The reviewer, human or Bugbot, approves when every item is present. The reasoning behind reviewing evidence instead of diffs is in evidence, not diffs.

When agent-written tests in Cursor go wrong

Section titled “When agent-written tests in Cursor go wrong”
SymptomLikely causeRecovery
Every new test passes on the first runTests were derived from the implementationRe-run the behaviour-first prompt with the rules stated and src/ read-only
A failing test goes green after the test file changed (toBe became toBeDefined)The agent weakened the assertionRevert the test file, say “The test is correct; fix the implementation”, and check git diff --stat
Tests pass but the feature is brokenOver-mocking: tests assert against the mockRun the hollow-mocks prompt; keep at least one integration or E2E test that crosses the real seam
Coverage rises, bugs still escapeTests execute lines without asserting outcomesRun the break-it check on the module that leaked the bug
Tests pass locally, fail in CIReal timers, random values, shared state or a different environmentAdd the rule’s no-timers, no-random line; reproduce with the CI command and seed data
The agent ran a command that deleted data or pushedA run mode that allows every commandSwitch to a mode that asks before destructive or remote commands; restore from the pre-session commit
Playwright tests fail at once with connection errorsThe dev server is not running or runs on another portStart the app, put the exact URL in the prompt, and tell the agent to wait for it to respond before navigating
A flaky test is “fixed” with a longer timeoutThe race is hidden, not removedAsk for the root cause with log evidence and 25 consecutive passing runs
The agent writes five features’ worth of code instead of making one test passThe loop is too wideInterrupt: “Stop. Make only the failing refund test pass, then run the suite.” A tight loop produces better code than one large generation
One test file grows past a few hundred linesOne file per service accumulates every domainAsk the agent to split it by behaviour (auth, refunds, webhooks) and run the suite before and after

Start from the Cursor test and debug overview if you need the section map. The same stage for other tools: testing with Claude Code and testing with Codex.