Dieser Artikel ist auf Englisch veröffentlicht.
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.
Der Pull Request trug den Titel „chore: remove jest, vitest config“ und entfernte 340 Zeilen sowie vier devDependencies. Für einen kurzen Moment befürchtete man, dass das CI sich beschweren würde – doch das geschah nicht. Jeder Test wurde ausgeführt, jeder Test bestand, die Abdeckungsberichterstattung funktionierte weiterhin einwandfrei, und der gesamte Ablauf war etwa um ein Drittel schneller als zuvor. In diesem Moment wurde klar, dass der eingebaute Testlaufmechanismus von Node stillschweigend aufgehört hatte, ein Experiment zu sein, und sich zu einer ernsthaften Alternative entwickelt hatte – einer Option, die aus reinem Gewohnheitsgrund über mehrere Jahre hinweg ignoriert worden war.
Auch handelte es sich hier nicht um eine Spielerei. Das Projekt verfügte über fast 180 Testdateien, die sowohl Unit- als auch Integrationstests abdeckten, inklusive wirklich schwieriger Mocking-Techniken für Timer und Fetch-Anfragen – genau die Art von Testumgebung, in der das Verwenden eines vollwertigen Frameworks wie die sichere Standardlösung erscheint.
Was tatsächlich geändert wurde
Das node:test-Modul tauchte erstmals als experimentelle Funktion in Node 18 auf und erhielt aus gutem Grund kaum Aufmerksamkeit: Die frühen Versionen verfügten weder über eine zuverlässige Mocking-Funktion noch über einen nutzbaren Watch-Modus sowie eine Coverage-Ausgabe, die eher absichtlich konzipiert schien als zusammengestückelt. Node 24 behebt den größten Teil dieser Probleme. Die Coverage-Daten stammen nun direkt von V8, ohne dass nyc oder c8 eingebunden werden müssen. Der Watch-Modus ist intelligent genug, um herauszufinden, welche Testdateien durch eine bestimmte Änderung betroffen sind, anstatt blind die gesamte Testsuite erneut auszuführen. Zudem sind das Mocking von Timern, Modulen und Funktionen bereits integriert, sodass kein zusätzlicher Abhängigkeitsbaum erforderlich ist, um beispielsweise eine Uhr zu simulieren oder eine Funktion zu stubben.
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { fetchUser } from './users.js';
test('fetchUser gibt normalisierte Daten zurück', 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);
});
Man führt es mit node --test aus – es gibt kein Konfigurationsfile, das gepflegt werden muss, keine Babel-Verarbeitung und auch keine ts-jest-Transformationsschicht, die jeder Datei leise ein paar Sekunden hinzufügt. Letzteres erwies sich als wichtiger als der Testausführer selbst.
Der native TypeScript-Teil
Node kann nun .ts-Dateien direkt ausführen, indem er die Typen beim Parsen entfernt anstelle sie im traditionellen Sinne zu kompilieren. Ab Node 24 wurde diese Funktion von einem experimentellen Flag zum Standard für die meisten Syntaxen. Es ist weder ts-node noch tsx sowie kein separater Build-Schritt mehr erforderlich, um Skripte oder Tests auszuführen.
node app.ts
node --test tests/
Es gibt hier einen echten Haken: Das Entfernen von Typangaben führt keinerlei Typüberprüfung durch. Es werden lediglich die Annotationen entfernt und der verbleibende Code ausgeführt. Wenn Ihre Codebasis auf Funktionen wie Enum-Typen mit Laufzeitwerten, Namensräumen oder Abkürzungen für Konstruktorparameter angewiesen ist, benötigen einige davon zusätzliche Flags oder werden noch nicht unterstützt, da sie tatsächlich JavaScript-Ausgabe erzeugen anstatt vollständig entfernt zu werden. Dies soll den TypeScript-Compiler ersetzen, und das versucht es auch nicht. Sie benötigen weiterhin tsc --noEmit oder Editor-Tools, um tatsächliche Typfehler aufzudecken. Was es hingegen beseitigt, ist die seit Langem bestehende Überlast durch das Kompilieren einer Datei nur zum Zweck ihrer Ausführung – eine Belastung, die stets im TypeScript-Arbeitsablauf enthalten war.
Warum die Testsuite schneller wurde
Durch das Entfernen von Jest wurde nicht nur eine Abhängigkeit beseitigt, sondern auch ein ganzes Transformationssystem. Standardmäßig handhabt Jest TypeScript auf eine von zwei Arten: Entweder wird die Verarbeitung an ts-jest übergeben, was gründlich ist, aber langsam, da es bei jedem Datei eine vollständige Typüberprüfung durchführt – es sei denn, man deaktiviert das ausdrücklich – oder es wird babel-jest verwendet, was schneller ist, aber eine eigene Konfigurationsschicht sowie eigene Probleme bei Dekoratoren und neuerer Syntax mit sich bringt. Wenn Node .ts-Dateien nativ ausführt, überspringt der Runner diesen Transformationsschritt ganz. Hinzu kommt, dass node:test Berichten zufolge bei ähnlichen Arbeitslasten etwa 40 % schneller ist als frühere Versionen von Nodes Testrunner; eine Reduzierung der Gesamtlaufzeit um ein Drittel wirkt dann nicht mehr wie Zufall, sondern ist tatsächlich die Kombination aus zwei separaten Geschwindigkeitsvorteilen.
Die Berichterstattung zur Abdeckungsrate war der Punkt, der zu Konflikten führen sollte. Das ist nicht passiert.
node --test --experimental-test-coverage tests/
Die Formatierung ist nicht so raffiniert wie das, was Istanbul standardmäßig in HTML erzeugt, aber die zugrunde liegenden Abdeckungsprozentsätze stimmten bis auf einen Punkt genau mit den Zahlen von c8 überein, und für die Freigabe von Pull Requests in CI ist genau diese Genauigkeit wirklich das Einzige, was zählt.
Wo es immer noch nachlässt
Snapshot-Testing ist hier die eigentliche Einschränkung. Teams, die stark auf Jests Snapshot-Funktion vertrauen – sei es für die Darstellung von Komponenten oder zur Erfassung der Struktur von API-Antworten – werden vorerst keinen integrierten Ersatz finden. Man muss entweder eigene Vergleichslogik schreiben oder eine separate Snapshot-Bibliothek einsetzen, um diesen Anwendungsfall abzudecken. Dasselbe gilt für alles, was auf Jests automatischem Modul-Emulieren mittels jest.mock('./path') und dessen Hoisting-Verhalten basiert. Nodes mock.method und mock.module übernehmen einen Großteil dieser Aufgaben, doch ihr Einsatz erfordert größere Klarheit und aktive Steuerung darüber, was genau an welchem Punkt ersetzt wird.
Für ein Projekt, das sich auf React-Komponenten mit umfangreichen UI-Tests basierend auf Snapshots konzentriert, sollte man mit einer deutlich holprigeren Umstellung rechnen als bei dem Backend-Dienst, der in dieser Bewertung verwendet wurde. Der Wechsel funktioniert derzeit nahezu reibungslos bei Server-seitigem Code sowie CLI-Tools.
Parallelisierte Ausführung ist ein weiterer Faktor, den man vor der Entscheidung für diesen Ansatz in einem umfangreichen Testumfeld überprüfen sollte. Jests Worker-Pool-Architektur verteilt die Testdateien nicht auf Prozesse auf dieselbe Weise wie Node’s eingebauter Runner, und je nach Aufbau des Testumfelds kann die Gesamtlaufzeit in einem großen Monorepo in eine unerwünschte Richtung abweichen – obwohl einzelne Testdateien isoliert gesehen schneller ausgeführt werden. Es lohnt sich, dies auf den tatsächlichen Maschinen zu messen, die Ihr CI-Pipeline verwendet, anstatt auf einem lokalen Laptop mit mehreren inaktiven Kernen und ohne Ressourcenkonkurrenz.
Wie die Migration tatsächlich abgelaufen ist
Für alle, die über einen solchen Umstieg nachdenken, verlief alles ungefähr wie folgt: Beide Testausführungen liefen parallel innerhalb des CI-Systems etwa eine Woche lang, anstatt alles auf einmal in einem einzigen Pull Request umzustellen. Die gleichen Testdateien wurden durch zwei separate CI-Aufgaben geprüft, wobei Ergebnisse und Laufzeiten direkt verglichen wurden. Dabei kamen zwei Tests zum Vorschein, die stillschweigend von jest-spezifischen Globalvariablen abhängig waren, an deren Einführung sich niemand mehr erinnerte; beide wurden sofort nach ihrer Identifizierung innerhalb einer Stunde behoben. Erst als die beiden CI-Aufgaben eine ganze Woche lang konstant übereinstimmende Ergebnisse lieferten, wurde der Pull Request zur Entfernung von Jest tatsächlich eingereicht. Es handelt sich dabei um eine langsame, wenig attraktive Vorgehensweise, doch wenn man das Sicherheitsnetz nutzt, das Fehler an anderen Stellen aufspüren soll, ist ein langweiliger, aber rückgängig machbarer Prozess besser als einer, der schnell ist, aber nicht rückgängig gemacht werden kann.
Was ist mit Vitest?
Dies ist ein Problem, dem man sich direkt stellen sollte, da es die Alternative ist, die die meisten Menschen zuerst erwähnen. Vitest läuft tatsächlich schneller als Jest, bietet eine angenehmere API und passt nahtlos in Vite-getriebene Frontend-Arbeitsabläufe – daran besteht kein Zweifel. Dennoch bleibt es eine zusätzliche Abhängigkeit mit eigener Konfiguration, die von der tatsächlich genutzten Node-Version abweichen kann und zu Fehlern führen kann, die einen ganzen Nachmittag in Anspruch nehmen. Wenn eine Frontend-Codebasis bereits um Vite herum aufgebaut ist, macht es weiterhin Sinn, Vitest zu wählen, da bisher noch nichts Natives die Komponententests im jsdom-Stil ersetzen kann. Für eine Backend-Dienstleistung oder ein Kommandozeilen-Tool ohne Bundler hat es jedoch an Attraktivität verloren, Vitest nur wegen einer besseren Assertionssyntax einzuführen, seit node:test in den Bereichen Mocking und Code-Coverage aufgeholt hat. Es handelt sich um ein Tool, das zu einer bestimmten Art von Projekt passt – nicht um einen vollständigen Ersatz für alles.
Die Einrichtung.
Was daraus resultiert
Es handelt sich dabei nicht um einen Aufruf, sofort jede bestehende Codebasis umzuschreiben. Für die Zukunft gibt es jedoch keinen klaren Standardgrund mehr, bei der Erstellung eines neuen Node-Dienstes einen externen Testausführer einzusetzen – eine Aussage, die vor einem Jahr noch nicht zutraf. Das Ökosystem hat fast zehn Jahre damit verbracht, umfangreiche Werkzeuge zu entwickeln, um Lücken zu schließen, die der Laufzeitumgebung selbst offen blieben. Da nun einige dieser Lücken geschlossen wurden, stellt ein Teil dieser Werkzeuge unnötiges Ballast dar. Es geht dabei nicht um die gesamte Toolchain, aber um einen größeren Anteil, als ursprünglich erwartet.
Weitere Literatur
- Die Brückenfunktion von TypeScript 6 auf dem Weg zu einem nativen TS 7-Compiler — Erfahren Sie, wie TypeScript 6 die Standardkonfigurationen, die Modulauflösung sowie die Import-Syntax aktualisiert, um Codebasen auf den schnelleren, auf Go basierenden TypeScript 7-Compiler vorzubereiten.
- Migrieren von Express-APIs zu Next.js App Router Route-Handlern — Lernen Sie, wie Express-Routen, Middleware sowie Datenmuster in den Next.js App Router mit Server Components umgewandelt werden können, einschließlich Überlegungen zur Bereitstellung.
any-Typen in React Hooks und JSX beseitigt werden.