Vitest is the natural testing companion for Vite projects. It reuses Vite's transform pipeline, giving you fast test execution with native ESM and TypeScript support. The API mirrors Jest, making migration straightforward. Even outside Vite projects, Vitest offers excellent performance.
Why Vitest?
| Feature | Jest | Vitest |
|---|---|---|
| Transform | Custom (babel-jest) | Vite's esbuild |
| ES Modules | Experimental | Native |
| TypeScript | Requires setup | Built-in |
| Watch mode | Slow on large projects | Instant |
| Config | Separate | Shares Vite config |
Vitest runs tests in the same environment as your app, eliminating configuration mismatches.
Installation
For Vite projects:
npm install -D vitest @vitest/ui jsdom
For React projects, add Testing Library:
npm install -D @testing-library/react @testing-library/jest-dom
Configuration
Extend your Vite config:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],
},
});
Or create a separate config:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
});
Setup file
Create a setup file for global configuration:
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
Package.json scripts
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
Writing tests
Basic test
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import { add, multiply } from './math';
describe('math utils', () => {
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('multiplies two numbers', () => {
expect(multiply(2, 3)).toBe(6);
});
});
React component test
// src/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Async tests
import { describe, it, expect } from 'vitest';
import { fetchUser } from './api';
describe('fetchUser', () => {
it('returns user data', async () => {
const user = await fetchUser('123');
expect(user).toEqual({
id: '123',
name: 'Alice',
});
});
it('throws on invalid id', async () => {
await expect(fetchUser('')).rejects.toThrow('Invalid ID');
});
});
Mocking
Mock functions
import { vi } from 'vitest';
const mockFn = vi.fn();
mockFn('arg');
expect(mockFn).toHaveBeenCalledWith('arg');
Mock modules
import { vi } from 'vitest';
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: '1', name: 'Mock' })),
}));
Mock timers
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('delays execution', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
Snapshot testing
it('matches snapshot', () => {
const { container } = render(<Button>Click</Button>);
expect(container).toMatchSnapshot();
});
// Inline snapshot
it('matches inline snapshot', () => {
expect(formatDate('2026-02-23')).toMatchInlineSnapshot(`"Feb 23, 2026"`);
});
Code coverage
Install coverage provider:
npm install -D @vitest/coverage-v8
Configure:
// vite.config.ts
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test/'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
}
Run:
npm run test:coverage
UI mode
The Vitest UI provides a visual test runner:
npm run test:ui
Features:
- Visual test tree
- Filter by status
- Watch mode controls
- Module graph view
Watch mode
By default, vitest runs in watch mode:
npm run test
- Re-runs affected tests on file change
- Press
ato run all tests - Press
fto run failed tests - Press
pto filter by file pattern - Press
qto quit
Testing hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Concurrent tests
Run tests in parallel:
describe.concurrent('parallel tests', () => {
it('test 1', async () => { /* ... */ });
it('test 2', async () => { /* ... */ });
});
Summary
Vitest provides fast, native ESM testing with a Jest-compatible API. Configure in vite.config.ts for Vite projects or standalone. Use Testing Library for React components. Mock with vi.fn() and vi.mock(). Enable coverage with @vitest/coverage-v8. Run vitest --ui for a visual interface. Watch mode makes TDD workflows smooth.
