Tests natifs de Node.js : mocks et minuteurs
Le lanceur natif fournit des mocks sans bibliothèque externe, via mock du module node:test ou via le contexte du test (t.mock), qui les restaure automatiquement à la fin du test.
Fonctions simulées
Section intitulée « Fonctions simulées »import { test } from 'node:test';import assert from 'node:assert/strict';
test('appelle le rappel pour chaque élément', (t) => { const rappel = t.mock.fn(); traiter([1, 2, 3], rappel);
assert.equal(rappel.mock.callCount(), 3); assert.deepEqual(rappel.mock.calls[0].arguments, [1]); assert.deepEqual( rappel.mock.calls.map((c) => c.arguments[0]), [1, 2, 3], );});
test('avec une implémentation', (t) => { const double = t.mock.fn((n) => n * 2); assert.equal(double(4), 8); assert.equal(double.mock.callCount(), 1);
double.mock.mockImplementationOnce(() => 100); assert.equal(double(4), 100); assert.equal(double(4), 8);});| Élément | Rôle |
|---|---|
fn.mock.callCount() |
Nombre d’appels |
fn.mock.calls |
Liste des appels (arguments, result, error, this) |
mockImplementation(f) |
Remplace l’implémentation |
mockImplementationOnce(f) |
Pour un seul appel |
resetCalls() |
Vide l’historique |
restore() |
Rétablit la fonction d’origine |
Espionner ou remplacer une méthode
Section intitulée « Espionner ou remplacer une méthode »test('journalise une erreur', (t) => { const erreur = t.mock.method(console, 'error', () => {}); // remplace traiterEntreeInvalide(); assert.equal(erreur.mock.callCount(), 1); assert.match(erreur.mock.calls[0].arguments[0], /invalide/);});
test('observe sans remplacer', (t) => { const espion = t.mock.method(calcul, 'additionner'); // appelle la vraie méthode calcul.additionner(1, 2); assert.deepEqual(espion.mock.calls[0].arguments, [1, 2]);});Avec t.mock, la méthode d’origine est rétablie à la fin du test. Avec mock importé de node:test, appeler mock.restoreAll() dans afterEach.
Simuler le temps
Section intitulée « Simuler le temps »import { test } from 'node:test';import assert from 'node:assert/strict';
test('expire après 30 secondes', (t) => { t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: new Date('2026-01-15T10:00:00Z'), });
const session = creerSession(); assert.equal(session.active, true);
t.mock.timers.tick(30_000); // avance l'horloge assert.equal(session.active, false); assert.equal(new Date().toISOString(), '2026-01-15T10:00:30.000Z');});| API | Rôle |
|---|---|
timers.enable({ apis, now }) |
Simule setTimeout, setInterval, setImmediate et/ou Date |
timers.tick(ms) |
Avance l’horloge |
timers.runAll() |
Exécute tous les minuteurs en attente |
timers.reset() |
Rétablit l’horloge réelle |
Avec des minuteurs de promesses (setTimeout de node:timers/promises), utiliser la variante asynchrone await t.mock.timers.tickAsync(1000) quand elle est disponible dans la version de Node.js utilisée.
Simuler fetch
Section intitulée « Simuler fetch »test('traite une réponse JSON', async (t) => { t.mock.method( globalThis, 'fetch', async () => new Response(JSON.stringify([{ id: 1, nom: 'Alice' }]), { status: 200, headers: { 'Content-Type': 'application/json' }, }), );
const clients = await listerClients(); assert.equal(clients.length, 1); assert.equal(globalThis.fetch.mock.callCount(), 1);});
test('traite une erreur serveur', async (t) => { t.mock.method( globalThis, 'fetch', async () => new Response('erreur', { status: 500 }), ); await assert.rejects(listerClients(), /HTTP 500/);});Variables d’environnement
Section intitulée « Variables d’environnement »import { afterEach, beforeEach, test } from 'node:test';
let ancienne;beforeEach(() => { ancienne = process.env.API_URL; process.env.API_URL = 'https://api.test';});afterEach(() => { process.env.API_URL = ancienne; if (ancienne === undefined) delete process.env.API_URL;});Toujours restaurer process.env : il est partagé par tout le processus de test.
Remplacer un module
Section intitulée « Remplacer un module »Les modules ES sont mis en cache à l’import. mock.module (option expérimentale, disponible selon la version de Node.js) permet de remplacer un module :
node --test --experimental-test-module-mocksimport { mock, test } from 'node:test';import assert from 'node:assert/strict';
test('remplace un module', async () => { const courriel = mock.module('../src/courriel.js', { namedExports: { envoyer: mock.fn(async () => true) }, }); const { inscrire } = await import('../src/inscription.js'); // importer APRÈS la simulation await inscrire('alice@exemple.com'); assert.equal(courriel.namedExports.envoyer.mock.callCount(), 1); courriel.restore();});Cette fonction étant expérimentale, la meilleure approche est souvent d’injecter les dépendances en paramètre :
export function creerService({ envoyerCourriel }) { return { async inscrire(email) { await envoyerCourriel(email); }, };}
test('inscrit et envoie un courriel', async (t) => { const envoyer = t.mock.fn(async () => {}); const service = creerService({ envoyerCourriel: envoyer }); await service.inscrire('alice@exemple.com'); assert.equal(envoyer.mock.callCount(), 1);});Aucun mécanisme de simulation de module n’est alors nécessaire : le code est plus simple à tester et à lire.
Fichiers et bases de données
Section intitulée « Fichiers et bases de données »Éviter de simuler le système de fichiers ou la base de données : utiliser des ressources jetables réelles.
import { mkdtemp, rm, writeFile } from 'node:fs/promises';import { tmpdir } from 'node:os';import { join } from 'node:path';import { test } from 'node:test';import assert from 'node:assert/strict';
test('lit un fichier de configuration', async (t) => { const dossier = await mkdtemp(join(tmpdir(), 'test-')); t.after(() => rm(dossier, { recursive: true, force: true }));
await writeFile(join(dossier, 'config.json'), JSON.stringify({ port: 3000 })); const config = await chargerConfig(join(dossier, 'config.json')); assert.equal(config.port, 3000);});Pour une base de données : SQLite en mémoire (node:sqlite, disponible dans les versions récentes de Node.js, ou better-sqlite3) ou une base dédiée aux tests ; voir SQLite : installation.
Bonnes pratiques
Section intitulée « Bonnes pratiques »- Simuler les frontières (réseau, horloge, courriel), pas la logique interne.
- Préférer l’injection de dépendances aux simulations de modules.
- Utiliser
t.mockpour la restauration automatique. - Ne pas laisser un minuteur simulé actif (
reset) : il bloque les tests suivants. - Vérifier ce que reçoit la dépendance simulée (arguments), pas seulement qu’elle est appelée.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
| La méthode n’est pas simulée | Référence conservée avant la simulation (import de la fonction destructurée) : appeler via l’objet |
| Un mock influence le test suivant | Utiliser t.mock (restauration automatique) ou mock.restoreAll() |
| Le test se bloque avec les minuteurs simulés | Promesses ou minuteurs non avancés : tick ou runAll |
mock.module is not a function |
Option --experimental-test-module-mocks ou version de Node.js plus ancienne |
fetch appelé réellement |
t.mock.method(globalThis, 'fetch', …) placé après l’appel, ou module qui capture fetch à l’import |
Date non simulée |
apis: ['Date'] absent de enable |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune propre à Ubuntu. Certaines fonctions de simulation sont récentes ou expérimentales : vérifier la documentation de la version de Node.js installée.