Claude Code Test Generation Prompt — Vitest + Behavior-First Template
A Claude Code prompt for generating Vitest unit, integration, and component tests. Tests behavior over implementation. Vite + React + TypeScript ready.
On this page
Why behaviour-first testing matters
A Vitest suite that snapshots useState internals breaks every refactor and tells you
nothing about whether the user-facing surface still works. A Vitest suite that asserts on
rendered output, on the resolved value of a fetch, and on the side effects of a click survives
internal restructuring and catches the regressions that actually ship. Claude Code,
unprompted, will write the first kind because it is what its training data is full of —
implementation-pinned tests that look thorough on disk and fall over the first time someone
extracts a hook. This prompt forces it to write the second kind: assertions on what the
exported behaviour does, not on which hook stores the state.
The prompt
# Test goal
Write Vitest tests for <module / component path>. Every assertion must
target exported behaviour or rendered output — never an internal hook,
private function, or implementation detail. Use Testing Library queries
that map to user perception (getByRole, getByLabelText, getByText) over
queries that target DOM internals (getByTestId).
# Required structure
- Group with `describe(<unit-under-test>, () => { ... })`
- One `it(...)` per behavioural claim, named "does X when Y"
- Arrange / act / assert visible inside each test
- Mock at the module boundary with `vi.mock(...)`; do not stub internals
# Determinism rules
- No real network: `vi.mock` fetch / axios / the API client module
- No real timers: `vi.useFakeTimers()` and advance explicitly
- No randomness: seed Math.random or stub the helper that calls it
- No date drift: mock Date.now() if the code reads the wall clock
# Coverage targets
- Happy path: at least one positive assertion
- Edge cases: empty input, null input, error response
- Re-render stability: identical input twice produces identical output
- Cleanup: any subscription / timer / event listener tears down
# Output format
1. The test file in full (full source, ready to drop into the repo).
2. The exact `pnpm test --run <file>` command and its truncated output.
3. One sentence per coverage target confirming a test exists for it. Why each section earns its place
The Test goal block carries the binding rule: assertions target exported behaviour. Naming
Testing Library queries explicitly is what stops Claude Code from reaching for data-testid
sprinkles that turn the test file into a pinning exercise. The Required structure block
enforces a shape a human can read at a glance — describe-then-it, arrange-act-assert
visible per test. The Determinism rules block is where most generated tests fail in CI even
when they pass locally; mocking at the module boundary plus fake timers plus seeded random
is the trio that makes a test pass on the second run, the third run, and the run a year
from now. The Coverage targets block is short on purpose — four bullets, each
non-negotiable, each easy for the agent to self-grade against.
Variants by test type
Pick the row that matches the unit under test. The verification command is what runs after the file is written — a passing run is the contract.
| Test type | Framework | Verification command |
|---|---|---|
| Unit | Vitest | pnpm test --run src/lib/<file>.test.ts |
| Integration | Vitest + Testing Library | pnpm test --run src/features/<dir> |
| Component | Vitest + React Testing Lib | pnpm test --run src/components/<file> |
| Playwright E2E | Playwright (separate suite) | pnpm exec playwright test --project chrome |
| Regression | Vitest | pnpm test --run src/regressions/<file> |
| Bug-reproduction | Vitest | pnpm test --run --reporter=verbose <file> |
For component tests, Testing Library’s getByRole should be the default and getByTestId
the last resort; if a piece of UI cannot be reached by role, that is itself an
accessibility finding worth flagging back through the
code review prompt. For Playwright, Claude Code’s
default is to over-mock the network and miss the real CSP / cookie / redirect issues — the
prompt above forbids real network for unit and integration but Playwright projects need
the network alive against a staging URL.
Common pitfalls when Claude Code writes tests
The agent’s three favourite anti-patterns are: testing useState directly via
renderHook even when the hook is private to the component — counter by demanding the
test mount the component and assert against rendered output. Snapshotting the entire DOM
tree when one assertion would do — counter by stating “no toMatchSnapshot; explicit
assertions only” in the prompt. Mocking at the wrong layer — for instance, stubbing
useEffect instead of stubbing the API module the effect calls — counter by naming the
exact module to mock at the boundary. Lock these decisions into
CLAUDE.md so they apply across every test
conversation, not only when you remember to paste the prompt. For the upstream tasks tests
verify, see the feature prompt, the
bug fix prompt, and the
refactor prompt — tests written with this prompt
are the safety net all three rely on.