Vitest — Démarrage rapide
Guide pratique pour installer, écrire et exécuter des tests JavaScript/TypeScript avec Vitest.
Ce document couvre les fondamentaux de Vitest. Pour la méthodologie générale (pyramide, TDD, mocking), voir Tests — Fondamentaux.
Qu’est-ce que Vitest ?
Vitest est un framework de tests optimisé pour l’écosystème Vite. Il offre :
- Une compatibilité API avec Jest (les mêmes
test,describe,expect) - Un moteur de transformation basé sur esbuild (démarre en quelques ms)
- Un watch mode natif et ultra-rapide
- Un support TypeScript et CSS in-line natif (plus besoin de babel-jest ou css-loader)
- Une couverture intégrée via Vite + Istanbul
Vitest est le choix par défaut pour les projets Vite, Next.js (avec le preset), Nuxt, SvelteKit et Remix.
Installation
# Si le projet utilise déjà Vite, Vitest s'installe en un clic
npx vitest --initCela ajoute vitest aux devDependencies et crée un fichier de configuration si nécessaire.
# Installation manuelle
npm install --save-dev vitest @vitest/coverage-v8Note :
@vitest/coverage-v8utilise le moteur V8 d’Istanbul. Pour les projets anciens,@vitest/coverage-c8(c8) reste une option valable.
Configuration
Cas 1 : Projet Vite (le plus courant)
Pas de fichier de config séparé nécessaire. La config Vitest hérite de vite.config.ts et se surcharge inline :
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test-setup.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/**/*.{d,test,spec}.ts']
}
}
});Cas 2 : Projet sans Vite (config autonome)
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
});Scripts package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}Raccourci utile :
vitest(sans argument) lance le watch mode immédiatement. C’est le flux de travail quotidien : un terminal pourvitest, un pour coder.
Structure de base
// calcul.test.ts
import { add, soustraire, multiplier } from './calcul';
describe('calcul', () => {
describe('add', () => {
it('additionne deux entiers positifs', () => {
expect(add(2, 3)).toBe(5);
});
it('additionne un nombre négatif', () => {
expect(add(-1, -1)).toBe(-2);
});
});
describe('soustraire', () => {
it('soustrait b de a', () => {
expect(soustraire(5, 3)).toBe(2);
});
});
describe('multiplier', () => {
it('multiplie deux nombres', () => {
expect(multiplier(3, 4)).toBe(12);
});
it('retourne zéro si l\'un des facteurs est nul', () => {
expect(multiplier(5, 0)).toBe(0);
});
});
});Convention :
describeregroupe les tests d’une même fonctionnalité,it(aliastest) déclare un cas unique. Le message passé àitdoit décrire le comportement attendu, jamaistest1.
Découverte des tests
Vitest suit les mêmes conventions que Jest :
| Pattern | Exemple |
|---|---|
| Fichier | *.test.ts, *.spec.ts, test/*, tests/* |
| Fonction | test() ou it() |
| Groupe | describe() ou context() |
# Lister les fichiers de test détectés
npx vitest list
# Exécuter tous les tests
npx vitest run
# Watch mode (mode interactif)
npx vitestTests paramétrés (test.each)
test.each() permet d’exécuter le même test avec plusieurs jeux de données sans dupliquer le code.
// tests/calcul.test.ts
import { add } from './calcul';
test.each([
[2, 3, 5],
[-1, -1, -2],
[0, 0, 0],
[100, -50, 50],
])('add(%i, %i) retourne %i', (a, b, attendu) => {
expect(add(a, b)).toBe(attendu);
});
// Avec le titre personnalisé
test.each([
{ nom: 'positifs', a: 2, b: 3, attendu: 5 },
{ nom: 'négatifs', a: -1, b: -1, attendu: -2 },
{ nom: 'avec zero', a: 0, b: 5, attendu: 5 },
])('add($nom) : add(%i, %i) → %i', ({ a, b, attendu }) => {
expect(add(a, b)).toBe(attendu);
});Convention : Un test paramétré échoue si l’un des jeux de données échoue. Le rapport indique le cas exact (index ou nom) qui a planté.
Compatibilité avec it et describe
test.each() fonctionne avec it et describe de la même manière :
it.each([
['abc', 3],
['hello', 5],
['', 0],
])('la longueur de "%s" est %i', (chaine, longueurAttendue) => {
expect(chaine.length).toBe(longueurAttendue);
});
describe.each([
{ role: 'admin', cree: true, supprime: true },
{ role: 'user', cree: false, supprime: false },
])('utilisateur $role', ({ role, cree, supprime }) => {
it('peut créer si autorisé', () => {
expect(permissions.peutCreer(role)).toBe(cree);
});
it('peut supprimer si autorisé', () => {
expect(permissions.peutSupprimer(role)).toBe(supprime);
});
});Règle :
test.eachcrée un test distinct par jeu de données. Cinq lignes dans le tableau = cinq tests exécutés séparément dans le rapport.
Assertions et matchers
Matchers courants
expect(valeur).toBe(5); // égalité stricte (===)
expect(valeur).toEqual({ a: 1 }); // égalité profonde (objet, tableau)
expect(valeur).toBeTruthy(); // truthy (1, "x", true, ...)
expect(valeur).toBeNull(); // === null
expect(valeur).toBeDefined(); // !== undefined
expect(valeur).toBeUndefined(); // === undefined
expect(valeur).toContain('x'); // sous-chaîne ou élément
expect(valeur).toHaveLength(3); // propriété .length
expect(valeur).toMatch(/abc/); // expression régulière
expect(valeur).toThrow(); // la fonction lève une exception
expect(valeur).toBeGreaterThan(3);
expect(valeur).toBeLessThanOrEqual(10);Matchers asynchrones
// Attendre qu'une promesse se résolve
await expect(promise).resolves.toBe('réussi');
// Attendre qu'une promesse rejette
await expect(promise).rejects.toThrow('erreur attendue');Tests conditionnels
// Exécuter un test uniquement si une condition est vraie
it.skip('non implémenté', () => {
// Sera ignoré
});
it.only('test isolé', () => {
// Seul ce test s'exécute dans le fichier
});Tests asynchrones
// 1. async/await (préféré)
it('résout la promesse', async () => {
const resultat = await fetcher.donnees();
expect(resultat).toEqual({ id: 1, nom: 'Alice' });
});
// 2. Retourner la promesse (style fonction)
it('résout la promesse (style retour)', () => {
return fetcher.donnees().then((resultat) => {
expect(resultat).toEqual({ id: 1, nom: 'Alice' });
});
});Règle : Préférer
async/await. Plus lisible, et l’erreur est signalée automatiquement si la promesse rejette.
Mocking
vi.fn() — Fonction mockée simple
const maFonction = vi.fn();
maFonction('a', 'b');
maFonction('c');
expect(maFonction).toHaveBeenCalledTimes(2);
expect(maFonction).toHaveBeenCalledWith('a', 'b');
expect(maFonction.mock.calls).toEqual([
['a', 'b'],
['c'],
]);Retourner une valeur
const retrieveUser = vi.fn().mockReturnValue({ id: 1, nom: 'Alice' });
const user = retrieveUser();
expect(user.id).toBe(1);vi.spyOn() — Espionner une méthode
import fs from 'fs';
it('lit le fichier', () => {
vi.spyOn(fs, 'readFileSync').mockReturnValue('contenu du fichier');
const resultat = monModule.traiterFichier();
expect(fs.readFileSync).toHaveBeenCalledTimes(1);
expect(fs.readFileSync).toHaveBeenCalledWith('/chemin/vers/fichier.txt');
});Piège :
vi.spyOn()appelle la fonction réelle par défaut. Il faut explicitement la remplacer avec.mockReturnValue()ou.mockImplementation()sinon le comportement réel s’exécute.
vi.mock() — Mock de module entier
// auth.ts
export const getUserFromToken = (token: string) => {
return fetch(`/api/users/${token}`).then(r => r.json());
};
// auth.test.ts
vi.mock('./auth', () => ({
getUserFromToken: vi.fn().mockResolvedValue({ id: 42, nom: 'Bob' }),
}));
import { getUserFromToken } from './auth';
it('renvoie l\'utilisateur à partir du token', async () => {
const user = await getUserFromToken('abc123');
expect(user.nom).toBe('Bob');
expect(getUserFromToken).toHaveBeenCalledWith('abc123');
});vi.hoisted() — Variables hoistées au niveau fichier
Utile quand une dépendance doit être mockée avant son import :
// test.ts
import { vi } from 'vitest';
// vi.hoisted() s'exécute AVANT les imports du fichier
const { mockService } = vi.hoisted(() => ({
mockService: { fetch: vi.fn() }
}));
vi.mock('./service', () => ({ default: mockService }));
import service from './service';Piège : En Vitest,
vi.mock()n’est pas hoistée automatiquement (contrairement à Jest). Utiliservi.hoisted()ou placer le mock en haut du fichier avant tout import.
Mocking partiel avec vi.mocked() (TypeScript)
import { getUserFromToken } from './auth';
import { vi } from 'vitest';
it('mock partiel', () => {
const mockedGetUser = vi.mocked(getUserFromToken);
mockedGetUser.mockResolvedValue({ id: 1, nom: 'Charlie' });
expect(mockedGetUser('xyz')).resolves.toEqual({ id: 1, nom: 'Charlie' });
});Fausses timers (vi.useFakeTimers)
Pour tester du code qui utilise setTimeout, setInterval ou Date.now sans attendre réellement, Vitest propose un système de fausses timers.
Cas 1 : mocker setTimeout / setInterval
it('appelle la fonction après le délai', async () => {
vi.useFakeTimers();
const callback = vi.fn();
setTimeout(callback, 1000);
// Aucun timer ne s'est exécuté
expect(callback).not.toHaveBeenCalled();
// Avancer de 500 ms
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
// Avancer au-delà du délai
vi.advanceTimersByTime(600);
expect(callback).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});Cas 2 : mocker setInterval
it('exécute l\'intervalle N fois', () => {
vi.useFakeTimers();
const callback = vi.fn();
setInterval(callback, 200);
vi.advanceTimersByTime(500);
// Exécuté à t=200 et t=400
expect(callback).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});Cas 3 : mocker Date.now()
it('utilise une date fixe', () => {
const dateFixe = new Date('2026-01-15T10:00:00Z');
vi.setSystemTime(dateFixe);
expect(Date.now()).toBe(dateFixe.getTime());
vi.useRealTimers();
});Nettoyage obligatoire
vi.useFakeTimers() remplace le timer global du runtime. Il faut toujours restaurer à la fin du test, sous peine de contourner les timeouts réels dans les tests suivants.
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});Piège : Oublier
vi.useRealTimers()dans un test = le prochain test qui utilisesetTimeouts’exécutera avec des timers fictifs et ne se terminera jamais (ou selon le faux délai). Toujours nettoyer dans unafterEach.
Piège :
vi.advanceTimersByTime(n)avance tous les timers en attente de n millisecondes. Si un timer est programmé à t+100 et un autre à t+200, avancer de 150 exécutera le premier mais pas le deuxième.
Snapshot testing
import { calculerTTC } from './taxe';
it('calcule le prix TTC correct', () => {
const result = calculerTTC(100, 0.2);
expect(result).toMatchInlineSnapshot(`120`);
});
// Ou avec fichier snapshot :
it('calcule le prix TTC (snapshot externe)', () => {
const result = calculerTTC(100, 0.2);
expect(result).toMatchSnapshot();
});Piège : Ne jamais valider un snapshot sans vérifier le contenu dans le commit. Un snapshot écrasé à l’aveugle peut faire passer un bug comme « corrigé ». Vérifier que le résultat correspond au comportement attendu avant de valider.
Watch mode et flags CLI
Vitest démarre en watch mode par défaut quand on lance vitest sans argument.
# Watch mode interactif (par défaut)
npx vitest
# Exécuter une fois et quitter
npx vitest run
# Watch mode + couverture
npx vitest --coverage
# Ne tester qu'un fichier
npx vitest run src/util.test.ts
# Ne tester qu'un pattern de nom
npx vitest run -t "addition"
# Désactiver le cache
npx vitest run --no-cacheInteractions en mode watch
| Touche | Action |
|---|---|
r | Relancer les tests modifiés |
a | Relancer tous les tests |
f | Ne tester que les fichiers échoués |
q | Quitter |
h | Afficher l’aide |
Rapports de couverture
# Générer la couverture (nécessite @vitest/coverage-v8)
npx vitest run --coverage
# Couleurs dans le terminal, JSON et HTML dans coverage/
# Rapport HTML ouvrable dans coverage/index.html// vite.config.ts — configuration couverture
export default defineConfig({
test: {
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/**/*.{d,test,spec}.ts']
}
}
});Environnements de test
Node (par défaut)
Exécution dans un environnement Node.js pur. Idéal pour la logique métier, les services, les utilitaires.
// Pas de configuration nécessaire, c'est le défautJSDOM (navigateur simulé)
Pour les tests frontaux ou toute logique qui dépend du DOM :
// vite.config.ts
export default defineConfig({
test: {
environment: 'jsdom'
}
});Compatibilité avec Jest
Vitest est compatible avec l’API de test de Jest : test, describe, it, beforeAll, afterEach, etc. Les assertions expect sont identiques.
Migration : Renommer
jest.fn()→vi.fn(),jest.mock()→vi.mock(),jest.spyOn()→vi.spyOn(). Le reste (structure, assertions, conventions) passe tel quel.
React Testing Library avec Vitest
npm install --save-dev @testing-library/react @testing-library/jest-dom// src/test-setup.ts
import '@testing-library/jest-dom';
// vite.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test-setup.ts']
}
});// src/Compte.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Compte } from './Compte';
describe('Compte', () => {
beforeEach(() => {
vi.spyOn(window, 'alert').mockImplementation(() => {});
});
it("affiche le nom de l'utilisateur", () => {
render(<Compte nom="Alice" premium={false} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
});
it('affiche le badge Premium si actif', () => {
render(<Compte nom="Bob" premium={true} />);
expect(screen.getByText('Premium')).toBeInTheDocument();
});
it("n'affiche pas le badge si non premium", () => {
render(<Compte nom="Charlie" premium={false} />);
expect(screen.queryByText('Premium')).not.toBeInTheDocument();
});
});Pièges courants
Piège :
vi.fn()retourneundefinedpar défaut. Pour renvoyer une valeur, utiliser.mockReturnValue(valeur)ou.mockResolvedValue(promise).
Piège : Contrairement à Jest,
vi.mock()n’est pas hoistée au sommet du fichier. Placer le mock avant tous les imports, ou utiliservi.hoisted()pour un contrôle explicite.
Piège :
toBe()pour les objets fait une égalité stricte sur la référence. UtilisertoEqual()pour comparer le contenu d’un objet ou d’un tableau.
Piège : L’ordre d’exécution des tests n’est pas garanti par Vitest (comme Jest). Ne pas s’appuyer sur l’ordre de lancement et nettoyer l’état dans un
beforeEach().
Piège CI : Vitest utilise un cache de transformation. En CI, passer
--no-cacheou vider le cache entre les builds pour éviter de tester du code périmé.
Piège avec
vi.hoisted(): Les variables définies dansvi.hoisted()ne sont pas accessibles en dehors. Les utiliser exclusivement pour le mocking et les importer dans le corps du fichier via la destructuration retournée.
Piège :
getBy*lève une erreur silencieuse qui fait passer le test au suivant sans échec visible. Toujours utiliserqueryBy*pour les assertions conditionnelles etgetBy*uniquement quand l’élément doit être présent.
Piège RTL :
renderne nettoie pas automatiquement le DOM entre les tests. Appelercleanup()dans unafterEachou utiliser le cleanup implicite de React Testing Library.