Explorer les tests natifs de Node.js avec test et assert
Node.js intègre désormais les briques essentielles pour écrire, exécuter et vérifier des tests sans installer Jest, Mocha ou une autre dépendance. Le module node:test, associé à node:assert/strict, fournit un socle léger, compatible avec les applications JavaScript et TypeScript, ainsi qu’avec les pipelines d’intégration continue.
Cette approche convient particulièrement aux projets qui cherchent à réduire leur chaîne d’outillage tout en conservant une expérience de test moderne : tests unitaires, scénarios asynchrones, suites imbriquées, filtrage, rapports et exécution parallèle. La documentation et les ressources de Développeur Web permettent ensuite d’élargir cette base vers Node.js, le cloud et les pratiques DevOps.
Comprendre le module node:test
Le runner de tests natif est accessible depuis le module intégré node:test. Dans les versions récentes de Node.js, il est suffisamment mature pour couvrir la plupart des besoins quotidiens d’un service backend ou d’une bibliothèque. Aucun fichier de configuration complexe n’est nécessaire pour lancer un premier scénario.
Un test peut être déclaré avec test() et recevoir une fonction de rappel. L’API accepte aussi une fonction asynchrone, ce qui permet d’attendre directement une promesse ou d’utiliser async et await. Le résultat est généralement affiché dans le format TAP, un standard lisible par les outils d’intégration continue.
import test from 'node:test';
import assert from 'node:assert/strict';
test('additionne deux nombres', () => {
assert.equal(2 + 3, 5);
});
Pour exécuter automatiquement les fichiers compatibles, utilisez la commande suivante :
node --test
Node recherche habituellement des fichiers dont le nom contient .test, .spec ou se trouve dans un répertoire test. Une commande ciblée peut aussi recevoir un chemin précis, ce qui facilite le débogage d’un scénario isolé.
Structurer une suite lisible
La méthode describe() permet de regrouper des tests appartenant à une même fonctionnalité. Elle améliore la lecture du rapport et autorise l’organisation de suites imbriquées. Les fonctions before, after, beforeEach et afterEach servent à préparer ou nettoyer un contexte de test.
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
describe('Panier', () => {
let panier;
beforeEach(() => {
panier = [];
});
it('est vide au démarrage', () => {
assert.deepEqual(panier, []);
});
});
Les tests doivent rester indépendants autant que possible. Une base de données temporaire, un fichier créé pendant l’exécution ou une variable globale mal réinitialisée peut provoquer des résultats incohérents. Les hooks sont utiles pour gérer ces ressources, mais ils ne doivent pas masquer des dépendances entre scénarios.
Le runner prend également en charge les tests désactivés, les tests marqués comme attendus en échec et les tests filtrés. Ces options peuvent être utiles durant une migration, mais un test ignoré doit rester visible dans le suivi du projet afin de ne pas devenir une dette permanente.
Vérifier les résultats avec node:assert
Le module node:assert/strict propose des assertions adaptées aux comparaisons de valeurs, d’objets, d’exceptions et de promesses rejetées. La variante stricte évite plusieurs conversions implicites et rend les erreurs plus prévisibles. Elle constitue un choix pertinent pour les nouveaux projets.
import assert from 'node:assert/strict';
assert.equal(10, 10);
assert.deepEqual(
{ nom: 'Ada', rôle: 'développeuse' },
{ nom: 'Ada', rôle: 'développeuse' }
);
assert.throws(
() => JSON.parse('{invalide}'),
SyntaxError
);
equal() compare des valeurs avec une sémantique stricte, tandis que deepEqual() examine récursivement les propriétés d’un objet ou les éléments d’un tableau. Pour les erreurs, throws() vérifie qu’une fonction synchrone échoue, et rejects() attend le rejet d’une promesse.
test('refuse un identifiant absent', async () => {
await assert.rejects(
() => chargerUtilisateur(),
{ name: 'ValidationError' }
);
});
Des messages d’assertion explicites facilitent l’analyse des échecs. Il est préférable de tester le comportement visible d’une fonction plutôt que son implémentation interne : valeur retournée, erreur produite, événement déclenché ou modification persistée.
Tester les opérations asynchrones
Les applications Node.js dépendent largement des entrées-sorties asynchrones : accès à PostgreSQL, requêtes HTTP, lecture de fichiers ou communication avec une file de messages. Le runner natif attend une promesse retournée par le test, ce qui évite les fonctions done et réduit les risques de test terminé trop tôt.
import test from 'node:test';
import assert from 'node:assert/strict';
test('lit une configuration JSON', async () => {
const configuration = await chargerConfiguration();
assert.equal(configuration.environnement, 'test');
assert.ok(configuration.port > 0);
});
Pour les timers, les délais et les ressources externes, la qualité du test dépend surtout de son isolation. Un faux serveur HTTP, une base éphémère ou une injection de dépendance rendent les scénarios plus rapides qu’un appel réel. Le module natif fournit certaines primitives, mais les besoins de simulation très avancés peuvent justifier une bibliothèque complémentaire.
Les tests en parallèle accélèrent une suite importante, à condition que les scénarios ne partagent pas d’état mutable. Un accès concurrent au même fichier ou à la même table peut créer des échecs intermittents. Commencez par des tests déterministes, puis activez la concurrence sur les groupes réellement indépendants.
Repères pour adopter l’outil
Le support natif convient aussi bien à une petite API qu’à un paquet open source. Il s’intègre à package.json, aux scripts npm et aux systèmes CI sans imposer une couche d’abstraction supplémentaire. Avant de migrer une suite existante, identifiez les fonctionnalités dont vous dépendez : mocks, couverture, snapshots ou reporters spécialisés.
Commandes utiles
node --testlance la découverte automatique des tests.node --test fichier.test.jsexécute un fichier ciblé.node --test --watchrelance les tests après une modification.node --test --test-name-pattern="panier"filtre par nom.
Assertions courantes
assert.equal()compare deux valeurs de manière stricte.assert.deepEqual()vérifie des structures imbriquées.assert.throws()valide une exception synchrone.assert.rejects()vérifie le rejet d’une promesse.
Le mode --watch est pratique pendant le développement, alors que le filtrage par nom réduit le temps d’attente lors d’une correction locale. Dans une équipe, documentez les conventions de nommage et les commandes principales afin que l’exécution reste identique sur les postes et dans le serveur CI.
Relier les tests au cycle de livraison
Dans un projet professionnel, les tests natifs doivent être exécutés avant le linting final, la construction de l’image Docker ou le déploiement. Une commande npm claire constitue souvent le meilleur point d’entrée :
{
"scripts": {
"test": "node --test",
"test:watch": "node --test --watch"
}
}
Le format TAP peut être consommé par plusieurs plateformes d’intégration continue. Pour mesurer la couverture, Node.js propose également des options natives selon la version utilisée, notamment avec --experimental-test-coverage dans certaines éditions. Vérifiez la documentation correspondant à la version installée avant de l’ajouter au pipeline.
Le choix d’un outil de test doit rester cohérent avec le reste du système. Par exemple, si une API dépend d’un moteur de recherche, les tests d’intégration peuvent révéler des différences importantes entre Algolia, Meilisearch et Typesense. Le runner lance les scénarios, mais la stratégie de données, les environnements éphémères et les contrats d’API déterminent la valeur réelle de la vérification.
Adoptez progressivement node:test : commencez par les fonctions pures, ajoutez les tests asynchrones, puis couvrez les flux intégrés. Pour partager un retour d’expérience ou signaler un sujet technique, utilisez la page contacter la rédaction.