Aller au contenu

Vitest : mocks, espions et temps

Un mock remplace une dépendance (réseau, base de données, horloge) pour rendre un test rapide, déterministe et isolé. L’objet vi fournit les outils.

import { expect, it, vi } from 'vitest';
it('appelle le rappel avec le résultat', () => {
const rappel = vi.fn();
traiter([1, 2, 3], rappel);
expect(rappel).toHaveBeenCalledTimes(3);
expect(rappel).toHaveBeenCalledWith(2);
expect(rappel).toHaveBeenNthCalledWith(1, 1);
expect(rappel.mock.calls).toEqual([[1], [2], [3]]);
});
it('simule des valeurs de retour', async () => {
const obtenir = vi.fn().mockReturnValueOnce('a').mockReturnValue('b');
expect(obtenir()).toBe('a');
expect(obtenir()).toBe('b');
const charger = vi.fn().mockResolvedValue({ id: 1 });
await expect(charger()).resolves.toEqual({ id: 1 });
const echouer = vi.fn().mockRejectedValue(new Error('réseau'));
await expect(echouer()).rejects.toThrow('réseau');
});
Méthode Rôle
mockReturnValue(v), mockReturnValueOnce(v) Valeur renvoyée (toujours, ou une seule fois)
mockResolvedValue(v), mockRejectedValue(e) Promesse résolue ou rejetée
mockImplementation(fn) Remplace le corps
mockClear() Vide l’historique des appels
mockReset() Vide l’historique et les comportements
mockRestore() Rétablit la fonction d’origine (avec spyOn)

Observer (ou remplacer) une méthode existante :

import { afterEach, expect, it, vi } from 'vitest';
afterEach(() => vi.restoreAllMocks());
it('journalise une erreur', () => {
const erreur = vi.spyOn(console, 'error').mockImplementation(() => {});
traiterEntreeInvalide();
expect(erreur).toHaveBeenCalledWith(expect.stringContaining('invalide'));
});
it('observe sans remplacer', () => {
const espion = vi.spyOn(calcul, 'additionner'); // appelle la vraie fonction
calcul.additionner(1, 2);
expect(espion).toHaveBeenCalledWith(1, 2);
});

Penser à restaurer (vi.restoreAllMocks() ou l’option restoreMocks: true de la configuration) : un espion oublié fausse les tests suivants.

import { expect, it, vi } from 'vitest';
import { chargerClient } from './api';
import { afficherNom } from './affichage';
vi.mock('./api'); // toutes les exports deviennent des fonctions simulées
it('affiche le nom du client', async () => {
vi.mocked(chargerClient).mockResolvedValue({ id: 1, nom: 'Alice' });
expect(await afficherNom(1)).toBe('Alice');
});

Avec une implémentation de remplacement :

vi.mock('./horloge', () => ({
maintenant: () => new Date('2026-01-15T10:00:00Z'),
}));
Point Détail
vi.mock est remonté en haut du fichier Il s’applique avant les imports, même écrit plus bas
Variables utilisées dans la fabrique Les déclarer avec vi.hoisted(...)
Garder une partie du module réel const reel = await vi.importActual('./module') dans la fabrique
Modules Node.js vi.mock('node:fs') (souvent préférable : tester avec un dossier temporaire)
const { envoyer } = vi.hoisted(() => ({ envoyer: vi.fn() }));
vi.mock('./courriel', () => ({ envoyer }));

Ne pas abuser des mocks de modules : un test qui simule tout ne vérifie plus rien. Préférer injecter les dépendances en paramètre (fonctions, classes) : on passe alors un faux sans vi.mock.

import { afterEach, beforeEach, expect, it, vi } from 'vitest';
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('traite une réponse JSON', async () => {
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify([{ id: 1, nom: 'Alice' }]), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
const clients = await listerClients();
expect(clients).toHaveLength(1);
expect(fetch).toHaveBeenCalledWith('/api/clients', expect.anything());
});
it('traite une erreur serveur', async () => {
vi.mocked(fetch).mockResolvedValue(new Response('erreur', { status: 500 }));
await expect(listerClients()).rejects.toThrow('HTTP 500');
});

Pour des scénarios réseau plus riches, la bibliothèque MSW (Mock Service Worker) intercepte les requêtes au niveau du réseau et sert de serveur factice partagé entre tests.

it("lit l'adresse de l'API", () => {
vi.stubEnv('API_URL', 'https://api.test');
expect(construireUrl('/clients')).toBe('https://api.test/clients');
});
afterEach(() => vi.unstubAllEnvs());

vi.stubEnv modifie process.env et import.meta.env pour la durée du test ; vi.unstubAllEnvs() restaure.

import { afterEach, beforeEach, expect, it, vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-15T10:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('expire après 30 secondes', () => {
const session = creerSession();
expect(session.active).toBe(true);
vi.advanceTimersByTime(30_000);
expect(session.active).toBe(false);
});
it('utilise la date simulée', () => {
expect(new Date().toISOString()).toBe('2026-01-15T10:00:00.000Z');
});
it('exécute les minuteurs asynchrones', async () => {
const promesse = attendreEtRepondre(); // utilise setTimeout
await vi.advanceTimersByTimeAsync(1000);
await expect(promesse).resolves.toBe('ok');
});
Méthode Rôle
useFakeTimers() Remplace setTimeout, setInterval, Date
setSystemTime(date) Fixe la date courante
advanceTimersByTime(ms) Avance l’horloge
advanceTimersByTimeAsync(ms) Idem, en laissant les promesses se résoudre
runAllTimers() Exécute tous les minuteurs en attente
useRealTimers() Rétablit l’horloge réelle

Toujours rétablir l’horloge réelle après le test : des minuteurs simulés oubliés bloquent les tests suivants.

export default defineConfig({
test: {
clearMocks: true, // vide l'historique des appels avant chaque test
restoreMocks: true, // restaure les espions
unstubEnvs: true, // restaure les variables d'environnement simulées
unstubGlobals: true, // restaure les globales simulées
},
});

Ces options évitent la plupart des fuites d’un test à l’autre.

Cas Préférer
Base de données Une base réelle jetable (SQLite en mémoire, conteneur) pour les tests d’intégration
Système de fichiers Un dossier temporaire (fs.mkdtemp)
Logique pure Aucune simulation nécessaire
Code que vous ne maîtrisez pas Tester le contrat avec un faux simple
Symptôme Piste
vi.mock ne remplace rien Chemin différent de celui importé par le code testé (comparer les chemins résolus)
ReferenceError dans la fabrique de vi.mock Variable non « hoisted » : vi.hoisted
Un espion influence les tests suivants restoreMocks: true ou vi.restoreAllMocks()
Le test se bloque avec les minuteurs simulés Promesses en attente : advanceTimersByTimeAsync
fetch is not a function Environnement ou version de Node.js ancienne : vi.stubGlobal('fetch', …)
Les appels du test précédent sont comptés clearMocks: true

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.