Aller au contenu

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.

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

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.

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/);
});
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.

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 :

Fenêtre de terminal
node --test --experimental-test-module-mocks
import { 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.

É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.

  • 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.mock pour 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.
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.