Vitest - Modern Test Framework
Status: Production Ready Last Updated: 2026-02-06 Vitest Version: 4.x Vite Compatibility: 6.x
Quick Start
Install
pnpm add -D vitest
Add to package.json
{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage" } }
Configuration
Minimal Config (vitest.config.ts)
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { globals: true, environment: 'node', }, });
React Config
import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], css: true, }, });
Cloudflare Workers Config
import { defineConfig } from 'vitest/config'; import { cloudflare } from '@cloudflare/vite-plugin';
export default defineConfig({ plugins: [cloudflare()], test: { globals: true, environment: 'node', // Workers tests often need longer timeouts for D1/KV testTimeout: 10000, }, });
Mocking Patterns
vi.mock - Module Mocking
import { vi, describe, it, expect } from 'vitest'; import { fetchUser } from './api';
// Mock entire module vi.mock('./api', () => ({ fetchUser: vi.fn(), }));
describe('User component', () => { it('fetches user data', async () => { // Type-safe mock implementation vi.mocked(fetchUser).mockResolvedValue({ id: 1, name: 'Test' });
// ... test code
expect(fetchUser).toHaveBeenCalledWith(1);
}); });
vi.spyOn - Spy on Methods
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Date handling', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-01')); });
afterEach(() => { vi.useRealTimers(); });
it('uses mocked date', () => { expect(new Date().getFullYear()).toBe(2026); }); });
vi.stubGlobal - Global Mocks
import { vi, describe, it, expect } from 'vitest';
describe('Environment', () => { it('mocks fetch globally', async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: 'test' }), });
vi.stubGlobal('fetch', mockFetch);
const response = await fetch('/api/test');
expect(mockFetch).toHaveBeenCalledWith('/api/test');
vi.unstubAllGlobals();
}); });
Snapshot Testing
Basic Snapshots
import { describe, it, expect } from 'vitest';
describe('Component output', () => { it('matches snapshot', () => { const result = renderComponent({ title: 'Hello' }); expect(result).toMatchSnapshot(); });
it('matches inline snapshot', () => {
const result = { name: 'test', count: 42 };
expect(result).toMatchInlineSnapshot( { "count": 42, "name": "test", } );
});
});
Update Snapshots
Update all snapshots
vitest run --update
Interactive update
vitest --ui
In-Source Testing
Test code directly in source files (tree-shaken in production):
// src/utils/math.ts export function add(a: number, b: number): number { return a + b; }
// In-source test block if (import.meta.vitest) { const { describe, it, expect } = import.meta.vitest;
describe('add', () => { it('adds two numbers', () => { expect(add(1, 2)).toBe(3); }); }); }
Config for in-source testing:
// vitest.config.ts export default defineConfig({ test: { includeSource: ['src/**/*.{js,ts}'], }, define: { 'import.meta.vitest': 'undefined', // Tree-shake in production }, });
Workspace Configuration (Monorepos)
// vitest.workspace.ts import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([ // Each package can have its own config 'packages/*/vitest.config.ts',
// Or define inline { test: { name: 'unit', include: ['src//*.test.ts'], environment: 'node', }, }, { test: { name: 'browser', include: ['src//*.browser.test.ts'], browser: { enabled: true, provider: 'playwright', name: 'chromium', }, }, }, ]);
Browser Mode Testing
// vitest.config.ts export default defineConfig({ test: { browser: { enabled: true, provider: 'playwright', // or 'webdriverio' name: 'chromium', headless: true, }, }, });
Install browser provider
pnpm add -D @vitest/browser playwright
Coverage
Install coverage provider
pnpm add -D @vitest/coverage-v8
Run with coverage
vitest run --coverage
// vitest.config.ts export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', ], thresholds: { statements: 80, branches: 80, functions: 80, lines: 80, }, }, }, });
Jest Migration
Key Differences
Jest Vitest
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
jest.useFakeTimers()
vi.useFakeTimers()
jest.clearAllMocks()
vi.clearAllMocks()
@jest/globals
vitest
Migration Steps
- Replace imports:
// Before (Jest) import { jest } from '@jest/globals';
// After (Vitest) import { vi } from 'vitest';
- Update config:
// jest.config.js → vitest.config.ts export default defineConfig({ test: { globals: true, // Enables describe/it/expect without imports environment: 'jsdom', }, });
- Replace jest. with vi.:
Quick replace (review changes carefully)
find src -name "*.test.ts" -exec sed -i 's/jest./vi./g' {} ;
Common Patterns
Testing Async Code
import { describe, it, expect } from 'vitest';
describe('async operations', () => { it('resolves promise', async () => { const result = await fetchData(); expect(result).toBeDefined(); });
it('rejects with error', async () => { await expect(failingOperation()).rejects.toThrow('Expected error'); }); });
Testing with Fixtures
import { describe, it, expect, beforeEach } from 'vitest';
describe('with fixtures', () => { let testData: TestData;
beforeEach(() => { testData = createTestFixture(); });
it('uses fixture', () => { expect(testData.id).toBeDefined(); }); });
Parameterized Tests
import { describe, it, expect } from 'vitest';
describe.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 },
])('double($input)', ({ input, expected }) => {
it(returns ${expected}, () => {
expect(double(input)).toBe(expected);
});
});
Debugging
Run Single Test
vitest run -t "test name" vitest run src/specific.test.ts
Debug Mode
With Node inspector
node --inspect-brk ./node_modules/vitest/vitest.mjs run
Or use Vitest UI
vitest --ui
Watch Mode
vitest # Watch mode (default) vitest run # Single run vitest watch # Explicit watch
Troubleshooting
"Cannot find module" in mocks
// Ensure mock path matches import path exactly vi.mock('./api'); // Matches: import { x } from './api' vi.mock('../api'); // Different! Won't work for './api' imports
ESM/CJS Issues
// vitest.config.ts - for CJS dependencies export default defineConfig({ test: { deps: { inline: ['problematic-cjs-package'], }, }, });
Globals Not Defined
// If using globals: true but TypeScript complains // Add to tsconfig.json: { "compilerOptions": { "types": ["vitest/globals"] } }
See Also
-
testing-patterns skill - General testing patterns
-
testing-library skill - React Testing Library integration
-
Official docs: https://vitest.dev