Strona główna / Artykuły / Replacing Jest with Node's Native Test Runner in Node 24

Artykuł opublikowany po angielsku.

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 słów

Zapytanie o pull request nosiło tytuł „chore: remove jest, vitest config” i usunęło 340 wierszy wraz z czterema elementami typu devDependencies. Na chwilę pojawiła się obawa, że system CI będzie narzekał, ale tak się nie stało. Wszystkie testy zostały wykonywane, wszystkie przeszły pomyślnie, raportowanie pokrycia kodu nadal funkcjonowało, a cały proces zakończył się o około jedną trzecią szybciej niż wcześniej. Wtedy stało się jasne, że wbudowany narzędzie do testowania w Node przestało być eksperymentem i stało się prawdziwą alternatywą – taką, którą przez kilka lat ignorowano z czystej przyzwyczajenia.

To nie był też prosty kod. Projekt zawierał blisko 180 plików testowych, obejmujących zarówno przypadki jednostkowe, jak i integracyjne, w tym dość skomplikowane mechanizmy symulacji zegarów i żądań fetch – dokładnie ten typ zestawu testów, w którym użycie pełnowartościowego frameworka wydaje się bezpiecznym standardem.

Co tak naprawdę się zmieniło

Moduł node:test po raz pierwszy pojawił się jako funkcja eksperymentalna w Node 18 i z dobrego powodu nie przyciągnął większej uwagi: w wczesnych wersjach brakowało solidnego wsparcia dla mockowania, użytecznego trybu obserwacji oraz wyników pomiaru pokrycia kodu, które wydawały się celowo zaprojektowane, a nie po prostu złożone przypadkowo. Node 24 naprawia większość tych problemów. Wyniki pomiaru pokrycia kodu pochodzą teraz bezpośrednio z V8, bez konieczności używania narzędzi takich jak nyc czy c8. Tryb obserwacji jest na tyle inteligentny, że potrafi określić, które pliki testowe są dotknięte daną zmianą, zamiast ślepo ponownie uruchamiać cały zestaw testów. Ponadto mockowanie timerów, modułów i funkcji jest wbudowane, więc nie ma potrzeby dodatkowych zależności tylko po to, by symulować zegar lub stworzyć sztuczną wersję funkcji.

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { fetchUser } from './users.js';
test('fetchUser zwraca spersonalizowane dane', 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);
});

Zapусkasz to za pomocą node --test — nie ma potrzeby utrzymywania pliku konfiguracyjnego, żadnej obróbki przez Babel, ani warstwy transformacji ts-jest, która cicho dodaje kilka sekund do każdego pliku. Ten ostatni punkt okazał się ważniejszy niż sam narzędzie do testowania.

Natywna część TypeScript

Node może teraz bezpośrednio wykonywać pliki .ts, usuwając typy w momencie analizy, zamiast kompilować je w tradycyjny sposób; począwszy od Node 24 ta funkcja przeszła z flagi eksperymentalnej na standard dla większości składni. Nie ma już potrzeby od używania ts-node, tsx ani osobnego kroku budowania, aby uruchomić skrypty lub testy.

node app.ts
node --test tests/

Istnieje tu poważny problem: usuwanie typów nie przeprowadza żadnej weryfikacji typów. Po prostu usuwa adnotacje i wykonywa kod, który pozostał. Jeśli twoja baza kodu polega na funkcjach takich jak enumy przechowujące wartości w czasie wykonywania, przestrzenie nazw czy skróty dla parametrów konstruktora, niektóre z nich wymagają dodatkowych flag lub jeszcze nie są obsługiwane, ponieważ faktycznie generują wynik w formacie JavaScript zamiast być całkowicie usuwane. Nie ma to na celu zastąpienia kompilatora TypeScript i tak też nie próbuje. Nadal potrzebujesz tsc --noEmit lub narzędzi edytora, aby wykryć rzeczywiste błędy typów. To, co eliminuje, to długo istniejący koszt dodatkowy związany z kompilowaniem pliku tylko po to, by go wykonać – opłata, która zawsze była częścią procesu pracy z TypeScript.

Dlaczego zestaw testów stał się szybszy

Pozbawienie się Jest oznaczało nie tylko usunięcie jednej zależności, ale także całą ścieżkę transformacji. Domyślnie Jest obsługuje TypeScript na dwa sposoby: albo przekazuje zadania do ts-jest, który jest dokładny, ale wolny, ponieważ przeprowadza pełną weryfikację typów we każdym pliku, chyba że wyraźnie to wyłączysz, albo używa babel-jest, który jest szybszy, ale dodaje własną warstwę konfiguracji oraz specyficzne problemy z dekoratorami i nowszą składnią. Gdy Node uruchamia pliki .ts w sposób natywny, sam narzędzie pomija ten krok transformacji. Do tego dochodzi fakt, że node:test jest, jak donoszono, o około 40% szybszy od wcześniejszych wersji narzędzia do testowania w Node przy podobnych obciążeniach, a jednokrotny spadek czasu wykonywania całego zestawu testów o jedną trzecią przestaje wyglądać na zbieg okoliczności – to w rzeczywistości efekt połączenia dwóch oddzielnych wzrostów szybkości.

Raportowanie zakresu pokrycia było elementem, który miał spowodować problemy. Tak się nie stało.

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

Formatowanie nie jest tak dopracowane jak to, co Istanbul generuje domyślnie w formacie HTML, ale podstawowe procenty pokrycia zgadzały się z danymi c8 z dokładnością do jednego procenta, a przy kontrolowaniu pull requestów w CI to właśnie taka dokładność jest najważniejsza.

Gdzie nadal ustępuje

Tutaj prawdziwym ograniczeniem jest testowanie na podstawie zdjęć stanu. Zespoły, które w dużej mierze polegają na funkcji tego typu w Jest, czy to do renderowania komponentów, czy do rejestrowania struktury odpowiedzi API, nie znajdą jeszcze wbudowanej alternatywy. Muszą albo sami napisać logikę porównawczą, albo zastosować oddzielną bibliotekę do tworzenia zdjęć stanu, aby obsłużyć ten przypadek użycia. To samo dotyczy wszystkiego, co opiera się na automatycznym symulowaniu modułów w Jest za pomocą jest.mock('./path') wraz z jego zachowaniem typu hoisting. Funkcje mock.method i mock.module w Node zajmują się większością tych zadań, ale ich użycie wymaga większej precyzji i jasności co do tego, co dokładnie zostanie zastąpione i w którym momencie.

W przypadku projektu skupionego na komponentach React z obszernymi testami interfejsu opartymi na snapshotach, można oczekiwać, że ta migracja będzie znacznie trudniejsza niż w przypadku usługi backend używanej w tej ocenie. Zamiana funkcjonuje niemal bezproblemowo obecnie w kodzie serwerowym oraz narzędziach CLI.

Kolejnym czynnikiem, który warto sprawdzić przed przyjęciem tego podejścia w dużym zestawie testów, jest możliwość równoległej eksploatacji. Architektura grupy pracowników w Jest nie rozdziela plików testowych pomiędzy procesy w taki sam sposób jak wbudowany silnik Node, a w zależności od struktury zestawu testów całkowity czas ich wykonywania może ulec niepożądanym zmianom w dużym monorepo, mimo że poszczególne pliki testowe działają szybciej w izolacji. Warto zmierzyć to na rzeczywistych maszynach używanych w pipeline’u CI, a nie na lokalnym laptopie z kilkoma wolnymi rdzeniami i bez konkurencji o zasoby.

Jak wyglądała w rzeczywistości migracja

Dla wszystkich, którzy rozważają tę zmianę, oto w przybliżeniu jak to przebiegało: oba narzędzia do wykonywania testów działały równolegle w środowisku CI przez około tydzień, zamiast zostać wdrożone jednocześnie w ramach jednego pull requesta. Te same pliki testowe były przetwarzane w dwóch oddzielnych zadańach CI, a wyniki oraz czas ich wykonywania były porównywane bezpośrednio. W trakcie tego procesu odkryto dwa testy, które potajemnie polegały na globalnych zmiennych specyficznych dla Jest, których nikt nie pamiętał, że zostały wprowadzone; obie te problemy zostały naprawione w ciągu godziny po ich zidentyfikowaniu. Dopiero gdy oba zadania CI przez cały tydzień dawały zgodne wyniki, został w końcu wysłany pull request usuwający Jest. To powolna i mało efektowna metoda, ale gdy mamy do czynienia z systemem bezpieczeństwa mającym na celu wykrywanie błędów w innych miejscach, proces nudny, ale możliwy do odwrócenia, jest lepszy niż szybki, ale niemożliwy do cofnięcia.

A co z Vitest?

Warto podjąć się tego bezpośrednio, ponieważ jest to alternatywa, o której najczęściej pierwszą wspominają ludzie. Vitest rzeczywiście działa szybciej niż Jest, oferuje przyjemniejszą API i naturalnie wpisuje się w procesy pracy z interfejsem użytkownika opartymi na Vite – nic z tego nie jest kwestionowane. Mimo to pozostaje dodatkową zależnością, wymagającą własnej konfiguracji, która może odbiegać od używanej wersji Node i powodować problemy trwające całe popołudnie. Jeśli baza kodu interfejsu użytkownika jest już zbudowana wokół Vite, wybór Vitest nadal ma sens, ponieważ żaden narzędzie natywne nie zastąpi jeszcze testowania komponentów w stylu jsdom. Natomiast w przypadku usługi backendowej lub narzędzia linii poleceń bez żadnego bundlera, wprowadzanie Vitest wyłącznie ze względu na lepszą składnię twierdzeń straciło na atrakcyjności, gdy node:test zamknął luki w zakresie mockowania i pomiaru pokrycia kodu. Jest to narzędzie odpowiednie dla określonego typu projektu, a nie pełna zamienna opcja dla wszystkiego.

Jak to wygląda w praktyce

To nie jest wezwanie do natychmiastowego przepisania całego istniejącego kodu. Jednak patrząc w przyszłość, nie ma już jasnego, domyślnego powodu, by używać zewnętrznego narzędzia do testowania przy uruchamianiu nowej usługi Node – stwierdzenie, które rok temu nie byłoby tak trafne. Ekosystem poświęcił prawie dziesięć lat na tworzenie złożonych narzędzi w celu wypełnienia luk pozostawionych przez sam runtime, a teraz, gdy niektóre z tych luk zostały zamknięte, część tych narzędzi stanowi niepotrzebny ciężar. Nie chodzi o cały zestaw narzędzi, ale o większą ich część, niż się spodziewano.

Pozycje pokrewne

  • Porównanie agentów AI Frontier: Astra, Flash, Fable i Mythos — Analiza tego, jak najnowsze wersje modeli GPT, Gemini i Claude radzą sobie z rzeczywistymi zadaniami agentywnymi, takimi jak programowanie, przeglądanie i używanie narzędzi, a nie tylko w testach benchmarkowych.
  • Dostosowywanie nowych funkcji kontrolowania fragmentacji w Turbopack w Next.js 16.3 — Praktyczny przegląd nowej konfiguracji turbopackChunking w Next.js 16.3, wyjaśniający, w jaki sposób maxChunkCountPerGroup i generateComponentChunks wpływają na rozmiar pliku pakietu oraz funkcjonowanie cache’u.
  • Przepisanie kompilatora TypeScript 7 na Go: co to oznacza dla bezpieczeństwa typów w React — Dowiedz się, jak kompilator TypeScript 7 oparty na Go przyspiesza proces budowania aplikacji i poprawia inferencję typów generycznych, eliminując ukryte typy any w hookach React i JSX.
  • Wewnątrz interceptorów NestJS: jak naprawić spadek wydajności o 96% na dużą skalę — Dowiedz się, jak pipeline wykonywania AOP w NestJS oraz problemy z rozwiązywaniem zadań w RxJS spowodowały gwałtowny wzrost wartości P99, oraz jak stworzyć interceptor do audytu bez zużycia pamięci w celu jego naprawy.