Accueil / Articles / Replacing Jest with Node's Native Test Runner in Node 24

Cet article est publié en anglais.

Node.jsTypeScriptTestingJestVitestPerformance

Replacing Jest with Node's Native Test Runner in Node 24

A real-world migration shows how Node 24's built-in test runner and native TypeScript support cut CI time while removing four dependencies.

1565 mots

La demande de fusion portait pour titre « chore: remove jest, vitest config », et elle a supprimé 340 lignes ainsi que quatre dépendances de développement. Il y a eu un bref moment d’inquiétude quant à une éventuelle plainte de la plateforme CI, mais rien de tel ne s’est produit. Chaque test a été exécuté avec succès, le rapport de couverture a continué de fonctionner correctement, et l’ensemble du processus s’est terminé environ un tiers plus rapidement qu’auparavant. C’est à ce moment-là que l’on a compris que le moteur de tests intégré à Node avait discrètement cessé d’être une expérience pour devenir une option sérieuse — une option qui avait été négligée par simple habitude pendant quelques années.

Il ne s’agissait pas non plus d’un codebase de démonstration. Le projet comptait près de 180 fichiers de tests, couvrant à la fois des cas unitaires et des cas d’intégration, avec des simulations particulièrement complexes concernant les temporiseurs et les requêtes fetch — exactement le type de suite où recourir à un framework complet semble être le choix par défaut le plus sûr.

Qu’est-ce qui a vraiment changé

Le module node:test est apparu pour la première fois en tant que fonctionnalité expérimentale dans Node 18, et pour de bonnes raisons il n’a pas reçu beaucoup d’attention : les premières versions manquaient d’un support solide pour le mockage, d’un mode watch utilisable, ainsi que de résultats de couverture qui semblaient avoir été conçus intentionnellement plutôt que assemblés à la hâte. Node 24 corrige la plupart de ces problèmes. La couverture provient désormais directement de V8, sans besoin d’installer nyc ou c8. Le mode watch est suffisamment intelligent pour déterminer quels fichiers de test sont affectés par une modification donnée, au lieu de relancer aveuglément l’ensemble des tests. De plus, le mockage des temporisateurs, des modules et des fonctions est intégré, ce qui élimine la nécessité d’une dépendance supplémentaire pour simuler une horloge ou remplacer temporairement une fonction.

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { fetchUser } from './users.js';
test('fetchUser retourne des données normalisées', async (t) => {
 const fetchMock = mock.method(global, 'fetch', async () =>
 new Response(JSON.stringify({ id: 1, name: 'test' }))
 ); const user = await fetchUser(1); assert.equal(user.id, 1);
 assert.equal(fetchMock.mock.callCount(), 1);
});

On l’exécute avec node --test — pas de fichier de configuration à gérer, pas d’étape Babel, aucune couche de transformation ts-jest qui ajoute silencieusement quelques secondes à chaque fichier. Ce dernier point s’est avéré plus important que le lanceur de tests lui-même.

La partie native de TypeScript

Node peut désormais exécuter directement les fichiers .ts en supprimant les types au moment de la parsing plutôt que de les compiler au sens traditionnel, et à partir de Node 24, cette fonctionnalité est passée d’un paramètre expérimental à la valeur par défaut pour la plupart des syntaxes. Il n’est plus nécessaire d’utiliser ts-node, ni tsx, ni de phase de compilation distincte simplement pour exécuter des scripts ou des tests.

node app.ts
node --test tests/

Il y a ici un vrai problème : le retrait des types ne effectue aucune vérification de type. Il se contente de supprimer les annotations et d’exécuter le code restant. Si votre base de code dépend de fonctionnalités telles que des enums contenant des valeurs en temps d’exécution, des noms d’espace ou des abréviations pour les paramètres de constructeur, certaines d’entre elles nécessitent des flags supplémentaires ou ne sont pas encore prises en charge, car elles génèrent en réalité du JavaScript plutôt que de pouvoir être complètement supprimées. Cet outil n’est pas conçu pour remplacer le compilateur TypeScript, et il ne tente même pas de le faire. Vous avez toujours besoin de tsc --noEmit ou d’outils d’édition pour détecter les erreurs de type réelles. Ce qu’il élimine, c’est la charge chronique liée à la compilation d’un fichier uniquement pour l’exécuter, une contrainte qui a toujours fait partie du flux de travail TypeScript.

Pourquoi l’ensemble de tests est devenu plus rapide

Se débarrasser de Jest n’a pas seulement supprimé une dépendance, mais aussi tout un pipeline de transformation. Par défaut, Jest gère TypeScript de deux manières : soit il confie la tâche à ts-jest, qui est approfondi mais lent car il effectue une vérification complète des types sur chaque fichier à moins que vous ne l’ayez explicitement désactivé, soit il utilise babel-jest, qui est plus rapide mais ajoute sa propre couche de configuration avec ses propres cas particuliers concernant les décorateurs et la syntaxe plus récente. Lorsque Node exécute nativement des fichiers .ts, le moteur d’exécution saute complètement cette étape de transformation. Ajoutez à cela le fait que node:test serait environ 40 % plus rapide que les versions précédentes du moteur de tests de Node pour des charges de travail similaires, et une réduction d’un tiers du temps total d’exécution des tests cesse de sembler être une coïncidence : il s’agit en réalité de deux améliorations de vitesse qui s’additionnent.

La génération de rapports de couverture était celle qui devait provoquer des problèmes. Ce ne fut pas le cas.

node --test --experimental-test-coverage tests/

Le formatage n’est pas aussi raffiné que celui produit par Istanbul en HTML par défaut, mais les pourcentages de couverture obtenus coïncidaient avec ceux de c8 à une seule décimale près, et pour valider les demandes de pull request dans l’CI, c’est précisément ce niveau de précision qui compte vraiment.

Où il reste insuffisant

Le test par capture d’écran constitue ici la véritable limitation. Les équipes qui comptent fortement sur la fonctionnalité de capture d’écran de Jest, que ce soit pour l’affichage des composants ou pour enregistrer la structure des réponses API, ne disposent pas encore de remplaçant intégré. Elles doivent donc écrire leur propre logique de comparaison ou utiliser une bibliothèque de capture d’écran distincte pour couvrir ce cas d’usage. Il en va de même pour tout ce qui repose sur le mock automatique des modules via jest.mock('./path') et son comportement de levée. Node propose mock.method et mock.module pour gérer une grande partie de ces tâches, mais leur utilisation exige d’être plus explicite quant à ce qui est remplacé et au moment précis où cela se produit.

Pour un projet axé sur des composants React accompagnés de tests d’interface abondants basés sur des captures d’écran, attendez-vous à ce que cette transition soit considérablement plus difficile que celle observée avec le service backend utilisé dans cette évaluation. Le remplacement fonctionne actuellement presque sans heurt dans le code côté serveur et les outils CLI.

L’exécution parallèle est un autre facteur à vérifier avant de choisir cette approche pour une suite de tests importante. L’architecture basée sur des groupes de travail de Jest ne distribue pas les fichiers de test entre les processus de la même manière que le lanceur intégré de Node, et en fonction de la structure de votre suite, le temps total d’exécution peut varier de manière négative dans un grand monorepo, même si chaque fichier de test s’exécute plus rapidement en isolation. Il est utile d’évaluer cela sur les machines réelles utilisées par votre pipeline CI plutôt que sur un ordinateur portable local disposant de plusieurs cœurs inutilisés et sans concurrence pour les ressources.

À quoi ressemblait réellement la migration

Pour ceux qui envisagent ce changement, voici à peu près comment cela s’est déroulé : les deux exécuteurs de tests ont fonctionné en parallèle au sein du CI pendant environ une semaine, plutôt que de passer tous en même temps dans une seule demande de fusion. Les mêmes fichiers de tests ont été traités par deux jobs CI distincts, dont les résultats et les temps d’exécution ont été comparés directement. Ce processus a révélé deux tests qui dépendaient silencieusement de variables globales spécifiques à Jest, que personne ne se souvenait avoir introduites ; ces problèmes ont tous deux été corrigés en moins d’une heure une fois identifiés. Ce n’est qu’après que les deux jobs CI aient donné des résultats cohérents pendant toute une semaine que la demande de fusion visant à supprimer Jest a été soumise. C’est une méthode lente et peu glamour, mais lorsqu’on manipule un mécanisme de sécurité destiné à détecter des erreurs ailleurs, un processus lent mais réversible vaut mieux qu’un processus rapide mais irréversible.

Et Vitest, alors ?

Cela mérite d’être abordé directement, car c’est l’alternative que la plupart des gens mentionnent en premier. Vitest est effectivement plus rapide que Jest, propose une API plus agréable et s’intègre naturellement dans les workflows frontend basés sur Vite — rien de tout cela n’est contestable. Néanmoins, il reste une dépendance supplémentaire, avec sa propre configuration, susceptible de s’éloigner de la version Node que vous utilisez réellement et de provoquer des pannes qui peuvent occuper toute une après-midi. Si un codebase frontend est déjà construit autour de Vite, choisir Vitest reste pertinent, puisqu’aucun outil natif ne remplace encore les tests de composants au style jsdom. En revanche, pour un service backend ou une outil en ligne de commande ne faisant pas appel à un bundler, l’intégration de Vitest uniquement pour une syntaxe d’affirmation plus agréable a perdu de son attrait depuis que node:test a comblé l’écart en matière de simulation et de couverture. C’est un outil adapté à un type particulier de projet, et non un remplacement complet pour tout.

Cette configuration n’est pas une invitation à réécrire immédiatement toute la base de code existante. Cependant, à l’avenir, il n’y a plus de raison évidente par défaut d’intégrer un exécuteur de tests externe lors du lancement d’un nouveau service Node, une affirmation qui n’aurait pas tenu il y a un an. L’écosystème a passé près de dix ans à développer des outils complexes pour combler les lacunes laissées par le runtime lui-même, et maintenant que certaines de ces lacunes ont été comblées, une partie de ces outils représente un fardeau inutile. Ce n’est pas l’ensemble de la chaîne d’outils, pour être clair. Juste une proportion plus importante que prévu.

Lectures complémentaires

  • Comparaison des agents IA Frontier : Astra, Flash, Fable et Mythos — Une analyse de la performance des dernières versions des modèles GPT, Gemini et Claude sur des tâches réelles impliquant l’usage d’agents, telles que le codage, la navigation et l’utilisation d’outils, et non seulement sur des benchmarks.
  • Ajustement des nouveaux contrôles de chunking de Turbopack dans Next.js 16.3 — Une présentation pratique de la nouvelle configuration turbopackChunking dans Next.js 16.3, expliquant comment maxChunkCountPerGroup et generateComponentChunks influencent la taille du bundle et le cacheage.
  • La réécriture en Go de TypeScript 7 : quelles conséquences pour la sécurité des types dans React — Découvrez comment le compilateur basé sur Go de TypeScript 7 accélère les builds et améliore l’inférence générique, éliminant ainsi les types any cachés dans les hooks React et JSX.
  • À l’intérieur des intercepteurs NestJS : comment corriger une régression de latence de 96 % à grande échelle — Apprenez comment le pipeline d’exécution AOP de NestJS et les problèmes liés à la désinstallation de RxJS ont provoqué une augmentation brutale de la latence P99, ainsi que comment créer un intercepteur d’audit sans allocation mémoire pour y remédier.