ITSkillsCenter
Développement Web

Tests unitaires avec Vitest en 2026 : tutoriel pas-à-pas

13 دقائق للقراءة

Vitest 4.1.5 a remplace Jest comme runner unitaire par défaut sur la grande majorité des nouveaux projets JavaScript et TypeScript. La raison principale est l intégration directe avec Vite, qui supprime la duplication de configuration que les développeurs ont longtemps subie : un tsconfig pour le code, un autre pour Jest, un Babel pour traduire entre les deux, et des plugins pour rattraper les écarts. Avec Vitest, tout passe par le pipeline Vite. Ce tutoriel montre pas-à-pas comment installer Vitest dans un projet existant, écrire les premiers tests, mocker proprement, mesurer la couverture et profiter du watch mode.

Prerequis

  • Node.js 20 ou plus. Active LTS Node 24 (Krypton) recommande.
  • Un projet JavaScript ou TypeScript existant, idéalement déjà sous Vite (Vue, Svelte, React+Vite, Astro). Sans Vite, l installation reste possible mais quelques options changent.
  • Niveau attendu : a l aise avec npm, modules ES, fonctions asynchrones.
  • Temps total : 1h30 a 2h pour parcourir et adapter au projet.

Étape 1 — Installer Vitest et configurer le runner

L’installation se fait en une seule commande. Vitest declare ses propres dépendances de transformation et n’a pas besoin de Babel. Si vous etes déjà sur Vite, le vite.config.ts est partagé avec Vitest, ce qui supprime toute configuration en double.

npm install --save-dev vitest @vitest/ui @vitest/coverage-v8

Trois paquets s installent ici. vitest est le runner principal. @vitest/ui ajouté une interface graphique navigable dans le navigateur, qui sert a debugger un test spécifique sans relancer toute la suite. @vitest/coverage-v8 active la mesure de la couverture de code via le moteur V8 natif de Node, plus rapide que l alternative Istanbul. On ajouté ensuite trois scripts dans package.json.

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

La distinction entre vitest (mode watch interactif) et vitest run (mode batch unique pour la CI) est importante. npm test en développement ouvre un mode interactif qui re-exécute les tests au moindre changement de fichier. npm run test:run exécute la suite une seule fois et sort avec le code 0 ou 1, ce qui convient aux pipelines de CI.

Étape 2 — Ecrire le premier test sur une fonction pure

Le premier test couvre une fonction utilitaire. Imaginons un petit module de calcul de remise dans src/lib/discount.ts. La fonction prend un montant et un code promo, retourne le montant après remise. C est le candidat idéal pour un test unitaire : entree en paramètres, sortie en valeur de retour, aucun effet de bord.

// src/lib/discount.ts
export function applyDiscount(amount: number, code: string): number {
  if (code === 'WELCOME10') return amount * 0.9;
  if (code === 'BLACKFRIDAY') return amount * 0.7;
  return amount;
}

On cree le test a cote du code teste, dans src/lib/discount.test.ts. La convention « test a cote du code » est explicitement encouragée par Vitest et facilité la navigation dans l IDE : on retrouve le test en deux clics depuis le code source.

// src/lib/discount.test.ts
import { describe, it, expect } from 'vitest';
import { applyDiscount } from './discount';

describe('applyDiscount', () => {
  it('applique 10% pour WELCOME10', () => {
    expect(applyDiscount(100, 'WELCOME10')).toBe(90);
  });

  it('applique 30% pour BLACKFRIDAY', () => {
    expect(applyDiscount(100, 'BLACKFRIDAY')).toBe(70);
  });

  it('ne fait rien pour un code inconnu', () => {
    expect(applyDiscount(100, 'INVALID')).toBe(100);
  });
});

Le lancement avec npm test affiche les trois tests verts en moins de 200 millisecondes. Le terminal reste ouvert et surveille les fichiers : si vous modifiez discount.ts, la suite se relance automatiquement. C est cette boucle ultra-rapide qui change le rapport au TDD : on écrit un test, on le voit échouer, on écrit l implementation, on le voit passer, sans attendre.

Étape 3 — Maîtriser les matchers essentiels

Un matcher est la fonction qui suit expect(). Le choix du bon matcher rend les messages d’erreur lisibles et évite les faux positifs. Vitest exposé la même API que Jest, donc tout ce qui suit est transferable. Les matchers les plus utiles au quotidien sont une douzaine.

// Egalite stricte de valeurs primitives
expect(2 + 2).toBe(4);

// Egalite profonde pour objets et tableaux
expect({ a: 1, b: [2] }).toEqual({ a: 1, b: [2] });

// Verifier l absence ou la nullite
expect(value).toBeUndefined();
expect(value).toBeNull();
expect(value).toBeTruthy();

// Tester une plage numerique
expect(score).toBeGreaterThan(0);
expect(score).toBeLessThanOrEqual(100);

// Verifier qu une chaine contient un substring ou matche un regex
expect(message).toContain('erreur');
expect(slug).toMatch(/^[a-z0-9-]+$/);

// Verifier qu une fonction lance une exception
expect(() => parseAmount('abc')).toThrow('format invalide');

// Verifier les arguments d un mock
expect(mockedFn).toHaveBeenCalledWith('arg1', 42);
expect(mockedFn).toHaveBeenCalledTimes(2);

La distinction entre toBe et toEqual est piégeuse pour les debutants. toBe compare avec Object.is, donc deux objets distincts qui ont le même contenu échouent. toEqual fait une égalité recursive structurelle. Pour les valeurs primitives (nombres, chaînes, booleens), toBe suffit. Pour les objets et tableaux, toEqual. Cette règle simple évite 80 % des faux negatifs.

Étape 4 — Tester un composant qui touche au DOM

Quand on teste un composant (React, Vue, Svelte), on a besoin d’un DOM. Vitest n’en fournit pas par défaut pour rester rapide en cas de tests purs. On installé happy-dom ou jsdom selon la préférence. happy-dom est plus rapide et suffit a 95 % des cas de figure ; jsdom est plus complet mais plus lent.

npm install --save-dev happy-dom @testing-library/dom

On configuré ensuite Vitest pour utiliser ce DOM. Si vous avez déjà un vite.config.ts, ajoutez la clé test dedans ; sinon créez vitest.config.ts.

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom',
    globals: true,
    setupFiles: ['./src/test-setup.ts'],
  },
});

L’option environment: 'happy-dom' dit a Vitest de simuler window, document et les APIs DOM standards dans Node. globals: true rend describe, it, expect disponibles sans import explicite, ce qui aere le code des tests. setupFiles pointe vers un fichier qui s exécute avant chaque suite, utile pour configurer Testing Library ou nettoyer les mocks.

Un test de composant React typique ressemble alors a ceci, en utilisant @testing-library/react.

import { render, screen } from '@testing-library/react';
import { Button } from './Button';

it('affiche le label et reagit au clic', async () => {
  const onClick = vi.fn();
  render(<Button label="Valider" onClick={onClick} />);
  await screen.getByRole('button', { name: 'Valider' }).click();
  expect(onClick).toHaveBeenCalledOnce();
});

Le test rend le composant dans un DOM virtuel, recupere le bouton par son role accessible (la même philosophie que Playwright), simule un clic, et vérifie que le handler a bien ete appelé une fois. Si le composant change de bibliothèque demain (passer de Material UI a Radix), le test continue de marcher tant que le role accessible reste un button.

Étape 5 — Mocker une dépendance avec vi.fn et vi.mock

Mocker veut dire remplacer une dépendance par une fausse implementation pour isoler le code teste. Deux APIs principales : vi.fn() cree une fonction espionne instantanee, vi.mock() remplace tout un module a l’import.

// Cas 1 : mocker un callback passe en parametre
import { processOrder } from './order';

it('appelle le callback de notification', () => {
  const notify = vi.fn();
  processOrder({ id: 42 }, notify);
  expect(notify).toHaveBeenCalledWith('Commande 42 traitee');
});

// Cas 2 : mocker un module importe en haut du fichier
vi.mock('./payment-gateway', () => ({
  charge: vi.fn(async () => ({ status: 'ok', tx: 'TX-123' })),
}));

import { checkout } from './checkout';
import { charge } from './payment-gateway';

it('charge le montant et retourne l ID de transaction', async () => {
  const result = await checkout({ amount: 100 });
  expect(charge).toHaveBeenCalledWith(100);
  expect(result.tx).toBe('TX-123');
});

Le premier cas mocke un callback simple : vi.fn() retourne une fonction qui memorise tous ses appels. Le second cas est plus puissant : on remplace intégralement le module ./payment-gateway avant que le code teste l importe, donc checkout appelle le mock sans le savoir. Cette technique permet de tester la logique de checkout sans toucher a la vraie passerelle de paiement, qui couterait de l argent et ralentirait la suite.

Le piège a connaitre : ne pas mocker trop. Si vous mockez tout, vous ne testez plus que vos mocks. Mockez la frontière du système — réseau, base de données, horloge — et laissez le code métier exécuter son chemin réel. Pour le mocking réseau plus réaliste, le tutoriel dédié a Mock Service Worker (MSW) montre une approche plus robuste.

Étape 6 — Mesurer la couverture

La couverture indique quel pourcentage du code est exécute par les tests. Ce n’est pas un indicateur de qualité — un code exécute n’est pas un code teste correctement — mais c’est un indicateur utile pour repérer les zones non touchées du tout.

npm run test:coverage

Le rapport s affiche dans le terminal avec un tableau par fichier et un total : pourcentage de lignes, branches, fonctions et statements couverts. Un rapport HTML détaillé est genere dans coverage/, ouvrable dans le navigateur. On y voit chaque ligne en vert (exécutée), rouge (non exécutée) ou jaune (branche partielle).

L’objectif n’est pas 100 % mais une couverture critique sur les modules sensibles : calculs métier, parseurs, validateurs. Pour les composants UI, 60 a 70 % suffisent souvent ; le reste est mieux servi par des tests E2E. On peut configurer un seuil minimal qui fait échouer la CI si on descend en dessous, dans vitest.config.ts sous test.coverage.thresholds.

Étape 7 — Profiter du mode UI pour debugger

Le mode UI ouvre une interface dans le navigateur a l adresse http://localhost:51204/__vitest__/. On y voit l arbre des tests, le statut, le code source, la sortie console et les graphes de durée. C est l’outil idéal pour debugger un test qui passe en local mais échoue en CI : on peut l isoler, le relancer, voir le diff exact entre la valeur attendue et la valeur reçue.

npm run test:ui

L’interface affiche aussi les tests qu’on a « skip » volontairement (avec it.skip), ce qui évite d oublier des tests desactives en attendant un fix. Pour la CI, on n active jamais le mode UI ; il sert uniquement au développement local.

Une dernière recommandation pratique vaut son pesant d’or sur la durée : nommer les tests pour qu’ils racontent une histoire lisible quand on parcourt le rapport. Au lieu de it('test 1'), ecrire it('refuse un panier vide avec 400'). Le rapport CI devient une spécification executable que n’importe quel membre de l’équipe peut relire pour comprendre ce que fait le code, sans plonger dans le source. Cette discipline coute trois secondes par test ecrit et fait gagner des heures dans les revues et les onboardings.

Erreurs fréquentes

Symptome Cause probable Solution
Erreur « Cannot use import statement outside a module » Vitest n’est pas configuré ou tsconfig en CommonJS Ajouter "type": "module" dans package.json ou utiliser tsconfig module: "ESNext"
Tests qui passent une fois sur deux Mocks non reinitialises entre tests Ajouter vi.clearAllMocks() dans beforeEach ou activer clearMocks: true dans la config
Coverage a 0 % Mauvaise config provider Vérifier provider: 'v8' et que @vitest/coverage-v8 est installé
Test asynchrone qui passe sans vérifier Oubli du await Toujours await les promesses, et utiliser expect.assertions(N) pour forcer N assertions
Snapshot qui change sans raison Date.now() ou Math.random() dans le rendu Utiliser vi.useFakeTimers() et seeder le random

Tutoriels associes

Bonnes pratiques approfondies

Au-delà de la mécanique de base, quelques disciplines distinguent une suite Vitest qui dure d’une suite qui pourrit. La première est de viser des tests indépendants : chaque test doit pouvoir tourner seul, dans n importe quel ordre, sans dependre d’un autre. Cette discipline tient sur deux pratiques. Premierement, ne pas partager d état entre tests via des variables au niveau du module — toujours initialiser dans beforeEach. Deuxiemement, reinitialiser tous les mocks entre tests via vi.clearAllMocks() ou l option clearMocks: true dans la config.

La deuxième discipline est la lisibilite du message d échec. Un test qui échoue doit dire en une ligne ce qui ne va pas. expect(result).toBe(true) donne « expected false to be true » — pas très utile. expect(result, 'la commande doit valider').toBe(true) ajouté le contexte directement dans le message. La troisième est l evitement des tests « trop intelligents » : si un test fait une boucle qui exécute un assertion 50 fois avec des valeurs différentes, c’est plus dur a debugger qu un test parametrise via it.each([...]) qui donne 50 noms distincts dans le rapport.

Snapshot tests : usage et limites

Vitest supporte les snapshot tests via expect(value).toMatchSnapshot(). La valeur est serialisee, comparée a un fichier de référence, et marquée différente si elle change. Cette technique est seduisante mais piégeuse : on cree facilement des snapshots enormes que personne ne relit lors des reviews, et qui derivent en silence vers un état incorrect — c’est comme ne pas avoir de test du tout.

La règle a tenir : utiliser les snapshots uniquement pour des structurés simples (un objet de configuration, une chaîne générée, un AST). Les éviter pour les rendus de composants HTML qui sont mieux testes via Testing Library en assertant le comportement, pas la structuré. Quand un snapshot change, prendre 30 secondes pour relire le diff avant de committer le nouveau — c’est la différence entre un test utile et un nuisible.

Ressources officielles

Pour la vue d’ensemble stratégique sur les tests modernes, voir le guide principal : Tests modernes en JavaScript en 2026.

Questions fréquentes

Vitest peut-il remplacer Jest sur un projet existant ?
Oui, et la migration prend en general deux jours sur un projet moyen. L’API publique est très proche, les principaux changements concernent jest.fn qui devient vi.fn, jest.mock qui devient vi.mock, et la configuration qui passe par vitest.config.ts.

Quelle différence entre describe et it ?
describe regroupé des tests qui partagent un contexte (un module, une fonction, un comportement). it (synonyme de test) décrit un cas précis. La convention est de lire la concatenation a haute voix : « describe applyDiscount, it applique 10% pour WELCOME10 ».

Comment tester du code qui utilisé Date.now ?
Avec vi.useFakeTimers() en debut de test, puis vi.setSystemTime(new Date('2026-01-01')). Toutes les références a Date.now retournent alors la valeur fixee, ce qui rend le test deterministe.

Faut-il committer le dossier coverage/ ?
Non. C est un artefact régénéré a chaque exécution. Ajoutez coverage/ et .vitest-cache/ a .gitignore.

Vitest fonctionne-t-il sans Vite ?
Oui, mais on perd l avantage du pipeline partagé. Pour un projet Next.js classique, Jest reste pertinent. Pour tout le reste, Vitest est plus simple a vivre.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité