Skip to Content
TestsVitest — Démarrage rapide

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 --init

Cela ajoute vitest aux devDependencies et crée un fichier de configuration si nécessaire.

# Installation manuelle npm install --save-dev vitest @vitest/coverage-v8

Note : @vitest/coverage-v8 utilise 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 pour vitest, 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 : describe regroupe les tests d’une même fonctionnalité, it (alias test) déclare un cas unique. Le message passé à it doit décrire le comportement attendu, jamais test1.

Découverte des tests

Vitest suit les mêmes conventions que Jest :

PatternExemple
Fichier*.test.ts, *.spec.ts, test/*, tests/*
Fonctiontest() ou it()
Groupedescribe() 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 vitest

Tests 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.each cré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). Utiliser vi.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 utilise setTimeout s’exécutera avec des timers fictifs et ne se terminera jamais (ou selon le faux délai). Toujours nettoyer dans un afterEach.

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-cache

Interactions en mode watch

ToucheAction
rRelancer les tests modifiés
aRelancer tous les tests
fNe tester que les fichiers échoués
qQuitter
hAfficher 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éfaut

JSDOM (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() retourne undefined par 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 utiliser vi.hoisted() pour un contrôle explicite.

Piège : toBe() pour les objets fait une égalité stricte sur la référence. Utiliser toEqual() 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-cache ou vider le cache entre les builds pour éviter de tester du code périmé.

Piège avec vi.hoisted() : Les variables définies dans vi.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 utiliser queryBy* pour les assertions conditionnelles et getBy* uniquement quand l’élément doit être présent.

Piège RTL : render ne nettoie pas automatiquement le DOM entre les tests. Appeler cleanup() dans un afterEach ou utiliser le cleanup implicite de React Testing Library.