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.

June 10, 2026by PromptGenius Team
vitestcursor-rulestestingunit-testvitejavascripttypescript
Vitest Cursor Rules: Next-Gen Unit Testing

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();
});