Skip to main content
rule

vitest — Cursor rule: reliable Vitest tests

A Cursor rule that tells the agent how to write Vitest unit and integration tests: where files live, how to isolate cases, how to mock.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/vitest.mdc
Open on GitHub

What vitest does

The rule covers day-to-day habits for Vite projects that test with Vitest. Test files belong next to the source they cover and follow the *.test.{ts,tsx,js,jsx} pattern, so editing a component keeps its test in view instead of buried in a separate tree.

Isolation runs on beforeEach and afterEach: fresh data before a case, cleanup after it. For async checks the rule asks for vi.waitFor rather than setTimeout — arbitrary waits are what turn a suite flaky on a slow machine or in CI. Mocking goes through vi.fn(), vi.spyOn() and vi.mock(), with vi.clearAllMocks() in afterEach so state does not leak into the next test.

Component tests should use the happy-dom environment in vite.config.ts or vitest.config.ts; reach for jsdom only when a browser API is missing there. Independent cases can be marked .concurrent, but then expect has to be destructured from the test context or snapshot and assertion tracking break.

The file lives at .cursor/rules/vitest.mdc and matches the globs **/*.{js,ts,jsx,tsx}.

What it helps with

  • settle where test files live and how they are named
  • remove flakiness from async checks with vi.waitFor
  • isolate cases so state stops leaking between tests
  • mock a whole module and swap one function inside it
  • speed up independent tests with .concurrent

Example prompts for the agent

  • write Vitest tests for this module and put the file next to the source
  • rewrite this test to use vi.waitFor instead of setTimeout
  • mock the lodash module and assert debounce was called once

How to install vitest

Manually: put the files in place

.cursor/rules/vitest.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about vitest

How do I install the vitest rule in Cursor?

Copy the file to .cursor/rules/vitest.mdc in your project root. Cursor picks it up for files matching the globs **/*.{js,ts,jsx,tsx}.

What does the vitest rule do?

It gives the agent a set of Vitest practices: co-located test files, isolation through hooks, vi.waitFor for async conditions, mocking via the vi API and the happy-dom environment.

Do I have to switch to happy-dom?

The rule recommends happy-dom as the faster option for most component tests and keeps jsdom for cases where a needed browser API is missing there.

Source text

vitest.mdc · CC0-1.0
---
description: This guide provides opinionated, actionable best practices for writing fast, reliable, and maintainable unit and integration tests using Vitest in modern JavaScript and TypeScript projects.
globs: **/*.{js,ts,jsx,tsx}
---
# Vitest Best Practices

Vitest is the definitive testing framework for our Vite-powered projects. It offers a fast, Jest-compatible API with deep integration into the Vite ecosystem. Adhering to these guidelines ensures our tests are robust, performant, and easy to maintain.

## 1. Code Organization & Naming

**Always co-locate test files with their source.** This improves discoverability and ensures tests are updated alongside their implementation.

*   **File Naming**: Use `*.test.{ts,tsx,js,jsx}`.
*   **Location**: Place test files directly next to the component or module they test.

❌ BAD:
```
// src/components/Button/Button.tsx
// tests/components/Button.test.tsx
```

✅ GOOD:
```typescript
// src/components/Button/Button.tsx
// src/components/Button/Button.test.tsx
```

## 2. Test Structure & Isolation

**Organize tests logically using `describe` and `it` (or `test`) blocks.** Ensure each test is isolated and deterministic.

*   **`describe`**: Group related tests into suites.
*   **`it` / `test`**: Define individual test cases. Prefer `it` for consistency with Jest.
*   **Hooks (`beforeEach`, `afterEach`)**: Use these for setup and teardown to ensure test isolation.

❌ BAD: (Shared state, no cleanup)
```typescript
let user;
test('creates user', () => {
  user = createUser();
  expect(user).toBeDefined();
});
test('updates user', () => { // Depends on previous test
  user.name = 'New Name';
  expect(user.name).toBe('New Name');
});
```

✅ GOOD: (Isolated tests with hooks)
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createUser, deleteUser } from './user-service';

describe('User Service', () => {
  let user;

  beforeEach(() => {
    user = createUser(); // Create a fresh user for each test
  });

  afterEach(() => {
    deleteUser(user.id); // Clean up after each test
  });

  it('should create a user', () => {
    expect(user).toBeDefined();
    expect(user.id).toBeTypeOf('string');
  });

  it('should update a user', () => {
    user.name = 'Jane Doe';
    expect(user.name).toBe('Jane Doe');
  });
});
```

## 3. Asynchronous Testing with `vi.waitFor`

**Always use `vi.waitFor` for polling conditions in asynchronous tests.** Avoid arbitrary `setTimeout` calls or manual polling loops. `vi.waitFor` is designed for reliable synchronization.

❌ BAD: (Flaky, relies on arbitrary timeout)
```typescript
test('data loads after delay', async () => {
  let data = null;
  fetchData().then(res => (data = res));
  await new Promise(resolve => setTimeout(resolve, 100)); // Arbitrary wait
  expect(data).toEqual('some data');
});
```

✅ GOOD: (Reliable polling with `vi.waitFor`)
```typescript
import { it, expect, vi } from 'vitest';
import { fetchData } from './api'; // Assume fetchData returns a Promise

it('should load data after delay', async () => {
  let data = null;
  fetchData().then(res => (data = res));

  // Polls until data is not null, with a 2-second timeout
  await vi.waitFor(() => expect(data).not.toBeNull(), { timeout: 2000 });

  expect(data).toEqual('some data');
});
```

## 4. Mocking Strategies

**Leverage Vitest's `vi` API for all mocking.** This provides Jest-compatible syntax and seamless integration. Always clean up mocks after each test.

*   **`vi.fn()`**: Mock individual functions.
*   **`vi.spyOn()`**: Spy on existing object methods.
*   **`vi.mock()`**: Mock entire modules.

### Function Mocking

❌ BAD: (Manual mock, no easy reset)
```typescript
const originalFetch = global.fetch;
global.fetch = () => Promise.resolve({ json: () => ({ id: 1 }) });
// ... test ...
global.fetch = originalFetch; // Easy to forget cleanup
```

✅ GOOD: (Using `vi.fn` with `afterEach` cleanup)
```typescript
import { it, expect, vi, afterEach } from 'vitest';
import { getUser } from './user-api';

// Mock the module containing fetchUser
vi.mock('./user-api', async (importOriginal) => {
  const mod = await importOriginal();
  return {
    ...mod,
    fetchUser: vi.fn(), // Mock specific function within the module
  };
});

// Import the mocked function after vi.mock
import { fetchUser } from './user-api';

afterEach(() => {
  vi.clearAllMocks(); // Clear mock calls after each test to prevent state leakage
});

it('should fetch user data', async () => {
  fetchUser.mockResolvedValueOnce({ id: 1, name: 'Test User' });
  const user = await getUser(1);
  expect(fetchUser).toHaveBeenCalledWith(1);
  expect(user.name).toBe('Test User');
});
```

### Module Mocking

**Mock modules at the top of the file.** This ensures the mock is applied before the module under test imports it.

✅ GOOD: (Module mock before imports)
```typescript
import { vi, it, expect } from 'vitest';

// Mock the entire 'lodash' module to control its behavior
vi.mock('lodash', () => ({
  debounce: vi.fn((fn) => fn), // Mock debounce to execute immediately
}));

import { debounce } from 'lodash'; // Import the mocked debounce
import { saveInput } from './input-handler'; // Module using debounce

it('should call save function without debounce delay', () => {
  saveInput('test');
  expect(debounce).toHaveBeenCalledOnce();
});
```

## 5. DOM Environment & Component Testing

**Use `happy-dom` for lightweight DOM environments.** It's generally faster and sufficient for most component tests. Switch to `jsdom` only if specific browser APIs are missing in `happy-dom`.

*   Configure in `vite.config.ts` or `vitest.config.ts`.

✅ GOOD: (Configuring `happy-dom`)
```typescript
// vite.config.ts or vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom', // Use happy-dom for faster DOM mocking
    globals: true, // Auto-import test APIs globally (e.g., describe, it, expect)
  },
});
```

## 6. Performance & Concurrent Tests

**Utilize `.concurrent` for tests that can run in parallel.** This significantly speeds up test suites where tests are independent.

*   Use `it.concurrent` for individual tests.
*   Use `describe.concurrent` for entire suites.
*   **Important**: When using `.concurrent`, always destructure `expect` from the test context to avoid issues with snapshot and assertion tracking.

❌ BAD: (Sequential tests, slow)
```typescript
describe('My Feature', () => {
  it('test A', async () => { /* ... */ });
  it('test B', async () => { /* ... */ });
});
```

✅ GOOD: (Concurrent tests, faster)
```typescript
import { describe, it } from 'vitest';

describe.concurrent('My Feature', () => {
  it('test A', async ({ expect }) => { // Destructure expect for concurrent tests
    expect(1).toBe(1);
  });

  it.concurrent('test B', async ({ expect }) => { // Destructure expect
    expect(2).toBe(2);
  });
});
```

## 7. Code Coverage

**Enable V8-based code coverage.** It offers near-zero overhead and integrates seamlessly.

*   Add `coverage` configuration to `vite.config.ts` or `vitest.config.ts`.
*   Run with `vitest run --coverage`.

✅ GOOD: (V8 coverage configuration)
```typescript
// vite.config.ts or vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom',
    globals: true,
    coverage: {
      provider: 'v8', // Use V8 for native, fast coverage
      reporter: ['text', 'json', 'html'], // Output formats for reports
      exclude: ['node_modules/', 'dist/', '.eslintrc.cjs'], // Exclude common directories from coverage
    },
  },
});
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

Everything for Testing and QA →

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor