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
AI Rules in Modern IDEs: Global and Project-Specific Configurations
AI rules customize AI assistants in modern IDEs like Cursor, Windsurf, and VSCode Copilot. Learn to configure global and project-specific rules for consistent, high-quality code.
Ruby Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Ruby that enforce Rails best practices, modern Ruby 3+ features, and clean code principles with AI assistance for secure, maintainable, production-ready applications.
AI Rule Best Practices: Configure, Manage, and Optimize
Master AI rule configuration for development. Learn best practices to implement, manage, and optimize AI rules, ensuring code quality, consistency, and enhanced developer workflows.