Write and run tests with Vitest, the Vite-native test framework. Use when a project uses Vite, has vitest in devDependencies, or vitest.config.* files.
Vitest is the test framework built on Vite. It gives you native ESM, TypeScript, and JSX transforms with zero config, a Jest-compatible API, and watch mode powered by Vite's HMR.
vitest is in devDependencies or vitest.config.ts existsnpm install -D vitestvitest.config.ts or add test block to vite.config.tssrc/**/*.test.ts using describe, it, expect from vitestnpx vitest (watch mode) or npx vitest run (single pass for CI)npm install -D @vitest/coverage-v8 then npx vitest run --coveragenpx vitest run (exits with non-zero on failure)# Core
npm install -D vitest
# Coverage (pick one)
npm install -D @vitest/coverage-v8
npm install -D @vitest/coverage-istanbul
# DOM environments (pick one)
npm install -D jsdom
npm install -D happy-dom
# UI dashboard
npm install -D @vitest/ui
# Browser mode
npm install -D @vitest/browser playwright
npx vitest # watch mode
npx vitest run # single run (CI)
npx vitest run src/utils # filter by path
npx vitest run --coverage # with coverage
npx vitest --ui # browser dashboard
npx vitest bench # benchmarks
npx vitest typecheck # type assertions
npx vitest run --reporter=json # JSON output
npx vitest run --changed HEAD~1 # only changed tests
npx vitest run --update # update snapshots
npx vitest run --bail 3 # stop after 3 failures
npx vitest list # list test files
npx vitest --project unit # specific workspace project
| Key | Action |
|---|---|
a | Re-run all tests |
f | Re-run only failed tests |
u | Update snapshots |
p | Filter by filename |
t | Filter by test name |
q | Quit |
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.{test,spec}.{ts,tsx,js,jsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
environment: 'node', // or 'jsdom', 'happy-dom'
globals: true, // no need to import describe/it/expect
setupFiles: ['./src/test/setup.ts'],
testTimeout: 10000,
pool: 'threads', // 'threads', 'forks', 'vmThreads'
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/**/*.d.ts'],
thresholds: { lines: 80, branches: 75, functions: 80, statements: 80 },
},
},
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
},
});
// @vitest-environment jsdom
import { it, expect } from 'vitest';
it('uses the DOM', () => {
const div = document.createElement('div');
expect(div.tagName).toBe('DIV');
});
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Calculator', () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it('adds numbers', () => {
expect(calc.add(2, 3)).toBe(5);
});
it('throws on divide by zero', () => {
expect(() => calc.divide(1, 0)).toThrow('Division by zero');
});
});
it('fetches users', async () => {
const users = await fetchUsers();
expect(users).toHaveLength(3);
});
it('rejects on error', async () => {
await expect(fetchBadUrl()).rejects.toThrow('Not Found');
});
it.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 0, expected: 0 },
])('doubles $input to $expected', ({ input, expected }) => {
expect(input * 2).toBe(expected);
});
describe.each([
{ env: 'development', debug: true },
{ env: 'production', debug: false },
])('in $env mode', ({ env, debug }) => {
it(`debug is ${debug}`, () => {
expect(getConfig(env).debug).toBe(debug);
});
});
it('matches snapshot', () => {
expect(renderConfig({ mode: 'prod' })).toMatchSnapshot();
});
it('matches inline snapshot', () => {
expect(formatName('alice')).toMatchInlineSnapshot(`"Alice"`);
});
Update snapshots: npx vitest run --update or press u in watch mode.
import { vi, it, expect } from 'vitest';
const fn = vi.fn();
fn('a'); fn('b');
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith('a');
// Return values
const mock = vi.fn()
.mockReturnValueOnce('first')
.mockReturnValue('default');
// Async
const asyncMock = vi.fn().mockResolvedValue({ ok: true });
import { vi, it, expect } from 'vitest';
import { getUser } from './api';
import { fetchFromDB } from './db';
vi.mock('./db', () => ({
fetchFromDB: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));
it('uses mocked db', async () => {
const user = await getUser(1);
expect(user.name).toBe('Alice');
expect(fetchFromDB).toHaveBeenCalledWith(1);
});
vi.mock() is auto-hoisted to the top of the file.
const obj = { greet: (n: string) => `Hi ${n}` };
const spy = vi.spyOn(obj, 'greet');
obj.greet('Alice');
expect(spy).toHaveBeenCalledWith('Alice');
spy.mockRestore(); // restore original
import { vi, afterEach } from 'vitest';
afterEach(() => vi.unstubAllGlobals());
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
}));
import { vi, it, expect, beforeEach, afterEach } from 'vitest';
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('advances timers', () => {
const fn = vi.fn();
setTimeout(fn, 5000);
vi.advanceTimersByTime(5000);
expect(fn).toHaveBeenCalledOnce();
});
import { afterEach, vi } from 'vitest';
afterEach(() => {
vi.restoreAllMocks(); // restore spies and mocked implementations
vi.unstubAllGlobals(); // restore stubbed globals
});
npm install -D @vitest/coverage-v8
npx vitest run --coverage
Config: