Vitest: Fast Unit Testing for Vite-Based JavaScript Projects
Jest has been the default JavaScript test runner for years. But it was designed for CommonJS and requires significant configuration to handle modern ESM, TypeScript, and Vite projects. Vitest was built from the start for the modern JavaScript ecosystem.
Vitest reuses your existing vite.config.ts, supports ESM and TypeScript out of the box, and runs tests in parallel with a watch mode that's dramatically faster than Jest's.
Why Vitest Over Jest?
| Feature | Vitest | Jest |
|---|---|---|
| ESM support | Native | Requires transform config |
| TypeScript | Native | Requires ts-jest or Babel |
| Vite config reuse | Yes | No |
| Watch mode speed | Very fast (HMR-based) | Moderate |
| Compatible API | Yes (Jest-compatible) | — |
| Browser testing | Yes (experimental) | No |
| Snapshot testing | Yes | Yes |
If you're using Vite already, Vitest is a near-zero-config drop-in. If you're using Jest, migration is usually minimal — Vitest's API is intentionally compatible.
Installation
# npm
npm install -D vitest
# pnpm
pnpm add -D vitest
# bun
bun add -d vitest
Add a test script to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
That's it — no config file needed for basic usage.
Your First Test
Vitest uses the same API as Jest: describe, it/test, expect, beforeEach, afterEach, etc.
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import { add, divide } from './math';
describe('math utilities', () => {
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Run with vitest (watch mode) or vitest run (single pass).
Configuration
Vitest can be configured in vite.config.ts or a dedicated vitest.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import { defineConfig as defineTestConfig } from 'vitest/config';
export default defineConfig({
// Your existing Vite config...
test: {
// Vitest options
globals: true, // Use global expect/describe without imports
environment: 'node', // or 'jsdom', 'happy-dom'
include: ['**/*.{test,spec}.{ts,tsx,js,jsx}'],
exclude: ['node_modules', 'dist'],
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules', '**/*.test.ts'],
},
},
});
Test Environments
Vitest supports multiple environments:
Node (default): For server-side code, utilities, and anything without DOM dependencies.
jsdom: Simulates a browser DOM. Good for testing React/Vue components without a real browser.
npm install -D jsdom
// vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
},
});
happy-dom: Faster jsdom alternative with good compatibility:
npm install -D happy-dom
You can also set the environment per-file using a comment:
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest';
Mocking
Vitest has a built-in mocking system:
Module mocks:
import { vi, describe, it, expect } from 'vitest';
// Mock an entire module
vi.mock('./database', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));
// Import after mocking
import { getUser } from './database';
describe('user service', () => {
it('fetches a user', async () => {
const user = await getUser(1);
expect(user.name).toBe('Alice');
expect(getUser).toHaveBeenCalledWith(1);
});
});
Spy on existing functions:
import { vi, describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
describe('file operations', () => {
afterEach(() => vi.restoreAllMocks());
it('reads a file', () => {
const spy = vi.spyOn(fs, 'readFileSync').mockReturnValue('mocked content');
const result = fs.readFileSync('test.txt', 'utf-8');
expect(result).toBe('mocked content');
expect(spy).toHaveBeenCalled();
});
});
Fake timers:
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('debounce', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('delays execution', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
});
Snapshot Testing
Vitest supports Jest-compatible snapshots:
import { describe, it, expect } from 'vitest';
import { renderComponent } from './utils';
describe('Button component', () => {
it('renders correctly', () => {
const html = renderComponent('<Button label="Click me" />');
expect(html).toMatchSnapshot();
});
});
First run creates the snapshot file. Subsequent runs compare against it. Update with vitest --update-snapshots.
Coverage Reports
npm install -D @vitest/coverage-v8
Run with coverage:
vitest run --coverage
Output in the coverage/ directory. Open coverage/index.html for a detailed breakdown by file and line.
Watch Mode
One of Vitest's best features is its watch mode, which uses Vite's module graph to only re-run tests affected by changed files.
vitest # starts in watch mode
Press h in watch mode for commands:
r— run all testsf— run only failed testsu— update snapshotsp— filter by test file patternt— filter by test name
Testing React Components
With @testing-library/react:
npm install -D @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('calls onClick when clicked', () => {
const onClick = vi.fn();
render(<Button label="Click me" onClick={onClick} />);
fireEvent.click(screen.getByText('Click me'));
expect(onClick).toHaveBeenCalledOnce();
});
});
Migrating from Jest
Vitest's API is Jest-compatible. Most migrations are just:
- Replace
jestimports withvitest:import { vi } from 'vitest'instead of using globaljest - Replace
jest.fn()withvi.fn(),jest.mockwithvi.mock, etc. - Remove Jest-specific transform config from your build tool
For TypeScript projects using ts-jest, Vitest is often faster to configure and runs faster — no transpilation step needed.
Summary
Vitest is the right default for modern JavaScript and TypeScript projects, especially those using Vite. The zero-config setup, native ESM and TypeScript support, and fast watch mode remove most of the friction from writing and running tests.
Start with vitest in watch mode while developing — the instant feedback loop makes test-driven development practical rather than painful.