Tests natifs de Node.js : asynchrone et sous-tests
Tests asynchrones
Section intitulée « Tests asynchrones »Une fonction de test peut être async : le test attend la fin de la promesse.
import { test } from 'node:test';import assert from 'node:assert/strict';
test('charge un client', async () => { const client = await chargerClient(1); assert.equal(client.nom, 'Alice');});
test('rejette un client inconnu', async () => { await assert.rejects(chargerClient(999), /introuvable/);});Toujours attendre les promesses (await ou return) : une promesse oubliée peut échouer après la fin du test, ou laisser le processus ouvert.
Sous-tests
Section intitulée « Sous-tests »Un test peut contenir des sous-tests (t.test), à attendre :
test('API clients', async (t) => { await t.test('liste', async () => { const liste = await lister(); assert.ok(Array.isArray(liste)); });
await t.test('création', async () => { const c = await creer({ nom: 'Alice' }); assert.equal(c.nom, 'Alice'); });});describe et it produisent la même hiérarchie avec une syntaxe plus lisible pour de nombreux cas. Un sous-test non attendu est annulé à la fin du parent : toujours await t.test(...).
Hooks d’un test
Section intitulée « Hooks d’un test »test('avec ressources', async (t) => { const base = await ouvrirBaseDeTest(); t.after(() => base.fermer()); // nettoyage à la fin du test t.beforeEach(() => base.vider()); // avant chaque sous-test
await t.test('insère', async () => { /* ... */ }); await t.test('lit', async () => { /* ... */ });});t.after est le bon endroit pour fermer serveurs, connexions et fichiers ouverts : il s’exécute même si le test échoue.
Serveur et ressources externes
Section intitulée « Serveur et ressources externes »import { after, before, describe, it } from 'node:test';import assert from 'node:assert/strict';import { app } from '../src/app.js';
describe('API', () => { let serveur; let base;
before(async () => { serveur = app.listen(0, '127.0.0.1'); await new Promise((resolve) => serveur.once('listening', resolve)); base = `http://127.0.0.1:${serveur.address().port}`; });
after(() => new Promise((resolve) => serveur.close(resolve)));
it('GET /sante', async () => { const reponse = await fetch(`${base}/sante`); assert.equal(reponse.status, 200); });
it('POST /clients refuse une entrée invalide', async () => { const reponse = await fetch(`${base}/api/clients`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); assert.equal(reponse.status, 400); });});Un serveur non fermé empêche le processus de se terminer : node --test finit par signaler un délai dépassé.
Tests paramétrés
Section intitulée « Tests paramétrés »Pas de test.each natif : boucler pour créer un test par cas.
const cas = [ ['alice@exemple.com', true], ['alice@', false], ['', false],];
for (const [entree, attendu] of cas) { test(`estEmailValide(${JSON.stringify(entree)}) vaut ${attendu}`, () => { assert.equal(estEmailValide(entree), attendu); });}Chaque cas apparaît comme un test distinct dans le rapport.
Délais et annulation
Section intitulée « Délais et annulation »test('opération lente', { timeout: 10_000 }, async () => { await operationLente();});
test('annulable', async (t) => { const controle = new AbortController(); t.signal.addEventListener('abort', () => controle.abort()); await fetch('http://127.0.0.1:3000/lent', { signal: controle.signal });});t.signal s’annule quand le test est interrompu (délai dépassé, arrêt de la suite). Le délai par défaut est illimité : fixer un timeout (ou --test-timeout) pour éviter les blocages en intégration continue.
Exécution concurrente
Section intitulée « Exécution concurrente »Par défaut, les fichiers de test s’exécutent en parallèle dans des processus séparés, et les tests d’un même fichier un par un. Pour faire tourner des tests indépendants simultanément :
describe('requêtes indépendantes', { concurrency: true }, () => { it('a', async () => { /* ... */ }); it('b', async () => { /* ... */ });});node --test --test-concurrency=2 # nombre de fichiers simultanésRéduire la concurrence quand les tests partagent une ressource unique (base de données, port, fichier).
Cibler les tests
Section intitulée « Cibler les tests »node --test --test-name-pattern="clients" # par nom (expression régulière)node --test --test-skip-pattern="lent"node --test --test-only # tests marqués { only: true }test('seul ce test', { only: true }, () => {}); // à ne jamais committeronly demande le drapeau --test-only : sans lui, tous les tests s’exécutent (pas de risque de tests désactivés par erreur en CI).
Fichier de préparation
Section intitulée « Fichier de préparation »Exécuter du code avant chaque fichier de test (variables d’environnement, simulations globales) :
node --import ./tests/setup.js --testprocess.env.NODE_ENV = 'test';process.env.TZ = 'UTC';Échecs fréquents des tests asynchrones
Section intitulée « Échecs fréquents des tests asynchrones »| Cause | Solution |
|---|---|
await manquant sur une assertion de promesse |
Toujours await assert.rejects(...) |
| Ressource non fermée (serveur, base, minuteur) | t.after, after ou afterEach |
| Sous-test non attendu | await t.test(...) |
| Tests qui s’influencent (données partagées) | Réinitialiser dans beforeEach ; base de test dédiée |
| Port fixe en conflit entre fichiers parallèles | Port 0 (choisi par le système) |
| Dépendance à l’heure ou à l’aléatoire | Simuler (voir Mocks et minuteurs) |
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
Promise resolution is still pending but the event loop has already resolved |
Promesse jamais résolue : vérifier les await, les rappels non appelés |
| Le processus ne s’arrête pas | Serveur, socket ou minuteur ouvert : fermer dans after |
test did not finish before its parent and was cancelled |
Sous-test non attendu |
| Tests qui passent seuls et échouent ensemble | État partagé entre fichiers ou tests (base, variables d’environnement) |
| Timeout en CI seulement | Machine plus lente : timeout plus généreux, moins de concurrence |
EADDRINUSE |
Port fixe : utiliser le port 0 |
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 : le comportement dépend de la version de Node.js.