Vitest Test Authoring Rules
Test-writing rules enforcing arrange-act-assert structure, behavior-based naming, strict mock boundaries, and minimal snapshots.
Use when: Attach to test files in Vitest projects to keep specs readable, deterministic, and focused on behavior rather than implementation details.
The skill file
Cursor · .cursor/rules/*.mdc.cursor/rules/*.mdc3139 chars
---
description: Conventions for writing and structuring Vitest tests
globs: "**/*.test.*"
alwaysApply: false
---
# Vitest rules
## Structure: arrange, act, assert
- Every test body reads in three blocks — setup, one action, assertions — separated by blank lines. No assertions interleaved with setup, no second "act" in the same test.
- One behavior per test. If the name needs "and", split it. Multiple `expect`s are fine when they describe one outcome (status + payload).
- `describe` blocks name the unit (`describe("createInvoice")`), nested `describe` for a scenario group (`describe("when the customer has no payment method")`). Max two levels of nesting.
- Shared setup goes in plain builder functions (`makeUser(overrides)`), not sprawling `beforeEach` mutation of suite-level `let` variables. `beforeEach` is for resets (`vi.restoreAllMocks()`, fake timers), not for building test data.
## Naming
- Test names state behavior and condition, present tense, no "should": `it("returns 404 when the slug does not exist")`, `it("trims whitespace before validating the email")`.
- Banned names: `it("works")`, `it("handles errors")`, `it("test createUser")`. If you can't name the expected behavior, you don't know what you're testing.
- Test file sits next to the unit it tests: `slug.ts` → `slug.test.ts`. Integration tests that span modules go in `__tests__/`.
## Mock boundaries
- Mock only at process boundaries: network (`fetch`/HTTP clients), database, filesystem, clock, randomness, environment. Never mock a sibling function in the module under test — if you need to, the module is doing too much; split it.
- Prefer dependency injection over `vi.mock` module patching. Reach for `vi.mock` only for third-party modules with no injection seam, and keep the factory minimal.
- Every `vi.fn()` you assert on must assert arguments, not just `toHaveBeenCalled()`. Call-count-only assertions test wiring, not behavior.
- Time and randomness are always controlled: `vi.useFakeTimers()` + `vi.setSystemTime()` for anything date-dependent; never let `Date.now()` leak into an assertion.
- `vi.restoreAllMocks()` in `afterEach` (or `restoreMocks: true` in config). A test must pass when run alone and in any order.
## Snapshots
- Inline snapshots (`toMatchInlineSnapshot`) only, max ~10 lines, and only for stable serialized shapes: error messages, generated SQL, small config objects.
- Never snapshot rendered component trees, full API responses, or anything with IDs/timestamps. A snapshot nobody can review by eye is a rubber stamp — replace it with explicit `expect` on the fields that matter.
- If a snapshot fails and your instinct is `--update` without reading the diff, the snapshot should not exist.
## Assertions
- Use the narrowest matcher: `toBe` for primitives, `toEqual` for objects, `toMatchObject` when extra keys are irrelevant, `toStrictEqual` when they are not.
- Async errors: `await expect(fn()).rejects.toThrow(NotFoundError)` — never try/catch with a boolean flag, never an assertion-free `.catch`.
- No conditional logic (`if`, loops with branching) inside a test. Parameterize with `it.each` instead.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursor/rules/testing-vitest-rules.mdc
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…