Introduction
Vitest is the premier modern testing tool in 2026, designed specifically for Vite. It delivers blazing performance thanks to native ESM module support, fast dependency resolution, and a familiar API inspired by Jest. Unlike traditional tools like Jest that can slow down on large projects, Vitest parallelizes tests by default and seamlessly integrates environments like JSDOM for React or Vue components.
Why adopt it? In a world where CI/CD demands rapid feedback, Vitest cuts test times by 50-80% compared to Jest. It handles snapshots, mocks, code coverage, and even an interactive test UI. This intermediate tutorial walks you through creating a Vite + React + TypeScript project, configuring Vitest, writing advanced tests (mocks, async, snapshots), and measuring coverage. By the end, you'll have a production-ready setup you'll bookmark for future projects. Ready to test like a pro?
Prerequisites
- Node.js 20+ installed
- Basic knowledge of Vite, React, and TypeScript
- An editor like VS Code with the Vitest extension
- Git for versioning the project
Create the base Vite project
npm create vite@latest vitest-tutorial -- --template react-ts
cd vitest-tutorial
npm installThis command initializes a Vite project with the React + TypeScript template. It installs all base dependencies. Avoid vanilla templates for this tutorial, as React enables realistic DOM tests; run npm install again to verify.
Install Vitest and testing dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @vitest/ui happy-dom happy-dom/test-environment
npm install -D @types/node @vitest/coverage-v8We add Vitest as a devDependency, along with RTL for React testing, JSDOM/HappyDOM for browser simulation, and coverage-v8 for reports. @vitest/ui provides a web-based interactive interface. Common pitfall: forgetting Node types for fs/path imports.
Configure vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()], test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test-setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules', 'dist', '**/*.config.*'],
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});This file extends the Vite config for tests: globals enables describe/it/expect without imports, jsdom simulates the DOM, and coverage generates reports. Add an '@/' alias for easy src imports. Pitfall: without plugins: [react()], JSX won't compile in tests.
Create the global test setup
import '@testing-library/jest-dom';This file runs before each test suite, extending expect with DOM matchers like toBeInTheDocument. Short and effective, it avoids repetitive imports. Pitfall: forgetting it breaks RTL assertions.
Add test scripts to package.json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test:unit": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}We add npm scripts to run tests (vitest), the UI (--ui), and coverage. Use vitest run for CI. Just copy the scripts section into your existing package.json. Pitfall: using npm test without defining it won't point to Vitest.
Create a component to test (counter)
import { useState } from 'react';
type CounterProps = {
initialValue?: number;
};
export function Counter({ initialValue = 0 }: CounterProps) {
const [count, setCount] = useState(initialValue);
return (
<div>
<h2>Compteur: {count}</h2>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(initialValue)}>Reset</button>
</div>
);
}This simple component uses useState for an incrementable counter. Optional props for flexibility. Perfect for testing renders, events, and state. Pitfall: missing keys on buttons can cause React warnings in batched tests.
Write basic unit tests
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter', () => {
it('affiche le compteur initial à 0', () => {
render(<Counter />);
expect(screen.getByText('Compteur: 0')).toBeInTheDocument();
});
it('incrémente le compteur', async () => {
render(<Counter />);
fireEvent.click(screen.getByText('+'));
expect(screen.getByText('Compteur: 1')).toBeInTheDocument();
});
it('réinitialise avec prop initialValue', () => {
render(<Counter initialValue={5} />);
fireEvent.click(screen.getByText('Reset'));
expect(screen.getByText('Compteur: 5')).toBeInTheDocument();
});
});These tests verify initial render, user interactions, and props. fireEvent simulates clicks. With globals: true, no need to import describe/it. Pitfall: using await without act() for useState updates; async here for consistency.
Advanced tests with mocks and snapshots
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
const mockConsole = vi.fn();
describe('Counter avancés', () => {
it('utilise un mock pour console.log', () => {
vi.spyOn(console, 'log').mockImplementation(mockConsole);
render(<Counter initialValue={10} />);
fireEvent.click(screen.getByText('-'));
expect(mockConsole).toHaveBeenCalledWith('Compteur décrémenté');
vi.restoreAllMocks();
});
it('snapshot du rendu initial', () => {
const { asFragment } = render(<Counter initialValue={42} />);
expect(asFragment()).toMatchSnapshot();
});
});We mock console with vi.spyOn (Vitest's native API), test calls, and use snapshots to validate JSX. vi.restoreAllMocks() cleans up. Add this after the basic tests. Pitfall: snapshots change with props; update with vitest -u.
Run and debug tests
Run npm run test:unit for watch mode (tests rerun automatically). npm run test:ui opens http://localhost:51204 to filter/focus tests. For coverage: npm run test:coverage generates coverage/coverage-final.json and index.html. Use --reporter=verbose for detailed traces.
Best practices
- Use
vi.mockfor external modules: Mock fetch/API before imports with dynamic paths (vi.mock('./api')). - Enable coverage thresholds in vitest.config.ts:
{ lines: 80, functions: 80 }to fail CI. - Prefer
userEventoverfireEventfor realistic events (type, hover). - Isolated tests: One
itper behavior, max 10 assertions. - Multi-project workspaces: Vitest supports
projects: ['./packages/*']for monorepos.
Common errors to avoid
- JSDOM not configured: 'document is undefined' errors → add
environment: 'jsdom'. - Missing imports in non-globals: If globals:false, explicitly import
describe/it/expect. - Unrestored mocks: Pollutes subsequent tests → always use
vi.restoreAllMocks()orafterEach. - Inaccurate coverage: Explicitly exclude node_modules/dist, use
provider: 'v8'for ESM.
Next steps
- Official docs: Vitest
- RTL guide: Testing Library
- Learni training on advanced testing: Master Vitest in enterprise with CI/CD and E2E.
- Example GitHub project: Fork this tutorial to experiment!