Vitest Cursor Rules: Next-Gen Unit Testing
Cursor rules for Vitest covering test structure, mocking with vi.fn/vi.mock, snapshot testing, code coverage, browser mode, workspace config, and performance optimization for fast unit tests.

Overview
Vitest is a next-generation testing framework powered by Vite, offering a Jest-compatible API with native ESM, TypeScript, and HMR support. These cursor rules enforce proper describe/it structure, explicit imports from vitest, vi.fn/vi.mock patterns, coverage configuration, browser mode, and workspace-based monorepo testing so AI assistants generate fast, maintainable Vitest tests.
Note:
Enforces explicit imports from vitest (not globals), vi.fn/vi.mock hoisting rules, onTestFinished cleanup, proper describe/it nesting, snapshot hygiene, coverage provider configuration, and workspace patterns for monorepos.
Rules Configuration
---
description: Enforces Vitest best practices including explicit imports, vi.mock hoisting rules, describe/it structure, onTestFinished cleanup, snapshot testing, coverage configuration, and monorepo workspace patterns.
globs: **/*.test.ts,**/*.spec.ts,**/*.test.tsx,**/*.spec.tsx,**/*.test.js,**/*.spec.js,vitest.config.ts,vitest.workspace.ts
---
# Vitest Best Practices
You are an expert in Vitest, test-driven development, and JavaScript/TypeScript testing.
You understand mocking, test isolation, coverage, and fast test execution patterns.
### Test Structure
- Import test utilities directly: `import { describe, it, expect, beforeEach, afterEach } from "vitest"`
- Avoid globals mode — explicit imports make test dependencies clear
- Use `describe` for grouping related tests
- Use `it` (or `test`) with descriptive names: `it("returns user by ID")`
- Nest describe blocks for hierarchical context
- Use `beforeEach`/`afterEach` for setup/teardown within their describe scope
- Use `beforeAll`/`afterAll` for one-time setup (database connections, server start)
- Place test files next to source files or in a parallel `__tests__` directory
### Mocking (vi.fn, vi.mock, vi.spyOn)
- Use `vi.fn()` to create mock functions, `vi.fn().mockReturnValue(value)` for return values
- Use `vi.fn().mockImplementation((arg) => { ... })` for custom behavior
- Use `vi.spyOn(object, 'method')` to spy on existing methods
- Always restore spies: `vi.restoreAllMocks()` in afterEach
- Place `vi.mock('module', factory)` at the top of the file — it is hoisted by Vitest
- Never put `vi.mock` inside `beforeEach` or `describe` — hoisting breaks scoping
- Use `vi.hoisted()` to define variables used inside `vi.mock` factory
- Use `vi.mock('./config', () => ({ API_URL: 'http://test.local' }))` for config mocking
- For module-level mocks: `vi.mock('fs', () => ({ readFileSync: vi.fn() }))`
- Use `vi.importActual()` inside mock factories to preserve real implementations
### Assertions & Matchers
- Use `expect(value).toBe(expected)` for primitive equality (===)
- Use `expect(value).toEqual(expected)` for deep object comparison
- Use `expect(array).toContain(item)` for array membership
- Use `expect(fn).toHaveBeenCalledWith(args)` for mock assertions
- Use `expect(fn).toHaveBeenCalledTimes(n)` for call count
- Use `expect(promise).resolves.toBe(value)` for async assertions
- Use `expect(promise).rejects.toThrow()` for error assertions
- Use `expect(value).toBeTypeOf("string")` for type checks
- Use `expect(value).toMatchInlineSnapshot()` for inline snapshots
### Coverage
- Set `coverage.provider: 'v8'` (default, fast) or `'istanbul'` (comprehensive)
- Configure `coverage.include: ['src/**/*.ts']` to limit coverage scope
- Configure `coverage.exclude: ['src/**/*.test.ts', 'src/types/**']`
- Set `coverage.thresholds` for CI gates: `{ branches: 80, functions: 80, lines: 80, statements: 80 }`
- Use `coverage.reporter: ['text', 'json', 'html']` for multi-format output
- Run coverage: `vitest run --coverage`
### Configuration (vitest.config.ts)
- Extend Vite config: `import { defineConfig, mergeConfig } from "vitest/config"`
- Set `test.globals: false` for explicit imports (recommended)
- Set `test.environment: 'node'` for backend tests, `'jsdom'` for frontend
- Configure `test.setupFiles: ['./vitest.setup.ts']` for global test setup
- Set `test.pool: 'threads'` (default, fast) or `'forks'` (isolated processes)
- Use `test.pool: 'forks'` for modules that leak between threads
- Configure `test.maxConcurrency` to limit parallel test execution
- Use `test.sequence.shuffle: true` to detect test ordering dependencies
### Cleanup & Teardown
- Use `onTestFinished(fn)` for per-test cleanup (preferred over afterEach for async cleanup)
- Clear mocks between tests: `vi.clearAllMocks()` in afterEach
- Close database connections and servers in afterAll
- Use `vi.useFakeTimers()` with `vi.useRealTimers()` in afterEach
- Use `vi.resetModules()` to clear module cache between tests
- Clean up timers: `vi.clearAllTimers()` in afterEach
- Always restore env vars after modification: `vi.stubEnv('KEY', 'value')` auto-restores on unstub
### Timer Mocking
- Use `vi.useFakeTimers()` to control time in tests
- Use `vi.advanceTimersByTime(ms)` to advance by a specific duration
- Use `vi.advanceTimersToNextTimer()` to jump to the next scheduled timer
- Use `vi.runAllTimers()` to execute all pending timers
- Always call `vi.useRealTimers()` in afterEach to restore
### Browser Mode
- Enable: `browser.enabled: true` in vitest.config.ts
- Requires `@vitest/browser` package with a provider (playwright, webdriverio, preview)
- Use `import { page } from '@vitest/browser/context'` for browser API access
- Locator API mirrors Playwright: `page.getByRole('button', { name: 'Submit' })`
- Assertions return promises: `await expect(locator).toBeVisible()`
- Screenshot testing: `await expect(page).toMatchScreenshot()`
- Runs tests in a real browser environment with full DOM, CSS, and JS execution
- Best for component testing and UI interactions that need a real rendering engine
### Parameterized Tests (test.each)
- Tabular format: `test.each([['input', 'expected']])('description', (input, expected) => { ... })`
- Object format: `test.each([{ input, expected }])('$input', ({ input, expected }) => { ... })`
- Template literal: `test.each`
a | b | expected
${1} | ${2} | ${3}
${4} | ${5} | ${9}
`('$a + $b = $expected', ({ a, b, expected }) => { ... })`
- Use `describe.each` for group-level parameterized setup
- Prefer `test.each` over forEach loops inside test blocks
### Workspace Configuration
- Create `vitest.workspace.ts` at the monorepo root
- Export array of project references: `export default ['packages/*', 'apps/*']`
- Each workspace project can have its own vitest.config.ts
- Run all workspaces: `vitest` from root
- Run specific workspace: `vitest --project=my-package`
- Workspace-aware coverage merges results across projects
- Use `test.workspace` option in config to define inline workspace projects
Installation
Create vitest.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
npm install -D vitest
# Add to package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
Examples
// user.service.ts — Source code to test
import { Database } from "./database";
export interface User {
id: number;
name: string;
email: string;
}
export async function getUserById(
db: Database,
id: number,
): Promise<User | null> {
const rows = await db.query("SELECT * FROM users WHERE id = ?", [id]);
return rows.length > 0 ? rows[0] : null;
}
export async function createUser(
db: Database,
user: Omit<User, "id">,
): Promise<User> {
const { insertId } = await db.query(
"INSERT INTO users (name, email) VALUES (?, ?)",
[user.name, user.email],
);
return { id: insertId, ...user };
}
// user.service.test.ts — Tests with mocking and coverage
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getUserById, createUser } from "./user.service";
import type { Database } from "./database";
describe("getUserById", () => {
let db: Database;
beforeEach(() => {
db = {
query: vi.fn(),
} as unknown as Database;
});
it("returns user when found", async () => {
const user = { id: 1, name: "Alice", email: "[email protected]" };
vi.mocked(db.query).mockResolvedValue([user]);
const result = await getUserById(db, 1);
expect(result).toEqual(user);
expect(db.query).toHaveBeenCalledWith(
"SELECT * FROM users WHERE id = ?",
[1],
);
});
it("returns null when user not found", async () => {
vi.mocked(db.query).mockResolvedValue([]);
const result = await getUserById(db, 999);
expect(result).toBeNull();
expect(db.query).toHaveBeenCalledTimes(1);
});
it("propagates database errors", async () => {
vi.mocked(db.query).mockRejectedValue(new Error("Connection lost"));
await expect(getUserById(db, 1)).rejects.toThrow("Connection lost");
});
});
describe("createUser", () => {
it("creates and returns the user with insertId", async () => {
const db = {
query: vi.fn().mockResolvedValue({ insertId: 42 }),
} as unknown as Database;
const result = await createUser(db, {
name: "Bob",
email: "[email protected]",
});
expect(result).toEqual({ id: 42, name: "Bob", email: "[email protected]" });
});
});
// vitest.config.ts — Configuration with coverage
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
include: ["src/**/*.test.ts"],
setupFiles: ["./vitest.setup.ts"],
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts", "src/types/**"],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
});
// vitest.setup.ts — Global test setup
import { afterEach, vi } from "vitest";
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
vi.useRealTimers();
});
Related Resources
Related Articles
Remix Cursor Rules: React Web Framework Guide
Cursor rules for Remix development covering nested routes, loader/action patterns, form handling, error boundaries, and server-side rendering with enhanced UX.
Redux Cursor Rules: State Management with Redux Toolkit
Cursor rules for Redux covering Redux Toolkit configureStore, createSlice patterns, Immer immutability, memoized selectors with createSelector, async thunks, RTK Query data fetching, typed hooks, and normalized state shapes.
C# Cursor Rules: .NET Development Best Practices
Cursor rules for C# and .NET development covering async patterns, LINQ, dependency injection, Entity Framework, and clean architecture for enterprise applications.