Головна / Статті / Replacing Jest with Node's Native Test Runner in Node 24

Статтю опубліковано англійською мовою.

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 слів

Запит на зміни мав назву „chore: remove jest, vitest config“, і в результаті було видалено 340 рядків разом із чотирма devDependencies. На мить здавалося, що CI почне скаржитися, але цього не сталося. Усі тести виконувалися, усі пройшли, звіти про покриття продовжували формуватися, а весь процес завершився приблизно на третину швидше, ніж раніше. Саме тоді стало зрозуміло, що вбудований інструмент тестування Node поступово перестав бути експериментом та став справжньою альтернативою — яку кілька років ігнорували через звичку.

Це також не була проста демонстраційна база коду. У проекті було майже 180 файлів тестів, які охоплювали як одиничні, так і інтеграційні тести, з використанням складних механізмів імітації таймерів та запитів fetch — саме такий набір тестів, де використання повнофункціонального фреймворку здається безпечним стандартом.

Що насправді змінилося

Модуль node:test вперше з’явився як експериментальна функція в Node 18, і не дарма він не привернув багато уваги: ранні версії не мали надійної підтримки мокінгу, придатного режиму спостереження та звітів про покриття, які виглядали як результат свідомого проектування, а не просто швидкого об’єднання елементів. Node 24 виправляє більшість цих проблем. Тепер звіти про покриття надходять безпосередньо від V8, без необхідності використання nyc чи c8. Режим спостереження достатньо розумний, щоб визначати, які файли тестів постраждають від певних змін, замість того щоб сліпо перезапускати весь набір тестів. Крім того, мокінг таймерів, модулів та функцій вбудований, тож немає потреби у додаткових залежностях для імітації годинника чи стабулювання функцій.

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { fetchUser } from './users.js';
test('fetchUser повертає нормалізовані дані', 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);
});

Ви запускаєте його за допомогою node --test — немає потреби у файлі конфігурації, немає етапу обробки через Babel, немає шару трансформації ts-jest, який би тихо додавав кілька секунд до кожного файлу. Останній момент виявився важливішим за сам інструмент для тестування.

Вбудована частина TypeScript

Тепер Node може безпосередньо виконувати файли .ts, прибираючи типи під час парсингу, замість того щоб компілювати їх у традиційному сенсі, і починаючи з Node 24 ця функція перейшла від експериментального параметра до стандарту для більшості синтаксисів. Не потрібні ts-node, tsx чи окремий крок компіляції лише для виконання скриптів чи тестів.

node app.ts
node --test tests/

Є одна серйозна проблема: процес видалення типів не здійснює жодної перевірки типів. Він просто видаляє анотації та виконує решту коду. Якщо ваша база коду залежить від таких функцій, як енумерації з значеннями під час виконання, простори імен чи скорочення параметрів конструктора, деякі з них потребують додаткових параметрів або ще не підтримуються, оскільки вони насправді створюють результат у форматі JavaScript, а не можуть бути повністю видалені. Цей інструмент не призначений для заміни компілятора TypeScript та навіть не намагається цього зробити. Для виявлення справжніх помилок типів вам все одно знадобиться tsc --noEmit або інструменти редактора. Однак він справді усуває довгострокове навантаження від компіляції файлу лише для його виконання — податок, який завжди був частиною процесу роботи з TypeScript.

Чому набір тестів став швидшим

Позбавлення себе Jest означало не лише видалення однієї залежності, а й усунення цілої системи трансформацій. За замовчуванням Jest обробляє TypeScript одним із двох способів: або передає завдання на обробку ts-jest, який є ретельним, але повільним, оскільки виконує повну перевірку типів у кожному файлі, якщо тільки ви не вимкнете цю функцію явно, або використовує babel-jest, який є швидшим, але додає власний рівень конфігурації з власними особливостями щодо декораторів та новішої синтаксису. Коли Node виконує файли .ts нативно, сам виконувач повністю пропускає цей крок трансформації. До цього додається той факт, що node:test, за повідомленнями, на 40% швидший за попередні версії виконувача тестів Node при схожих навантаженнях, а скорочення загального часу виконання тестів на третину вже не здається випадковістю — це насправді сума двох окремих покращень швидкості.

Звітування про рівень покриття мало стати джерелом проблем, але цього не сталося.

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

Форматування не настільки вдосконалене, як те, що Istanbul створює за замовчуванням у HTML, але базові відсотки покриття збігалися з цифрами c8 до однієї десятої, і для контролю пул-запитів у CI саме цей рівень точності є найважливішим.

Де воно все ще поступається

Тестування знімків є справжньою обмеженням у цьому випадку. Команди, які сильно покладаються на функцію знімків у Jest — чи то для відтворення компонентів, чи для фіксації формату відповідей API — поки що не знайдуть вбудованої альтернативи. Їм доводиться писати власну логіку порівняння або використовувати окрему бібліотеку знімків лише для вирішення цієї проблеми. Те саме стосується всього, що будується на автоматичному імітуванні модулів у Jest за допомогою jest.mock('./path') та його механізму підняття коду. Функції mock.method та mock.module у Node вирішують багато з цих завдань, але їх використання вимагає більшої чіткості та прямого контролю над тим, що саме замінюється та в який момент.

Для проекту, зосередженого на компонентах React із численними тестами користувацького інтерфейсу на основі знімків стану, очікуйте, що цей перехід буде значно складнішим, ніж у випадку бекенд-сервісу, який використовувався під час цієї оцінки. Заміна працює майже безперешкодно зараз у коді серверної частини та інструментах CLI.

Паралельна експекутація — ще один фактор, який варто перевірити перед тим, як обрати цей підхід для великої низки тестів. Архітектура пулу робочих процесів Jest не розподіляє файли тестів між процесами так само, як це робить вбудований запускач Node, і залежно від структури вашої низки тестів загальний час виконання може змінитися у небажаний бік у великому монорепозиторії, навіть якщо окремі файли тестів виконуються швидше ізольовано. Варто виміряти це на справжніх машинах, які використовує ваш CI-пайплайн, а не на локальному ноутбуці з кількома вільними ядрами та без конкуренції за ресурси.

Як насправді виглядала міграція

Для тих, хто розглядає таку заміну, ось приблизний перебіг подій: обидва засоби тестування працювали паралельно в середовищі CI протягом приблизно тижня, замість того щоб миттєво перейти на новий варіант у одному запиті про зміни. Ті самі файли тестів проходили обробку у двох окремих завданнях CI, причому результати та час виконання порівнювалися безпосередньо. У результаті цього процесу було виявлено два тести, які потайки використовували глобальні змінні, характерні для Jest, про існування яких ніхто не пам’ятав; обидва проблеми були виправлені протягом години після їх виявлення. Лише тоді, коли обидва завдання CI протягом цілого тижня давали схожі результати, було подано запит про зміни для видалення Jest. Це повільний та не надто привабливий спосіб, але коли йдеться про механізм захисту, призначений для виявлення помилок в інших частинах коду, нудний, але справедливий процес кращий за швидкий, але неможливий до скасування.

А що з Vitest?

Це варто розглянути без відкладень, адже це альтернатива, про яку більшість людей згадує першою. Vitest дійсно працює швидше за Jest, пропонує більш зручний API та легко інтегрується у робочі процеси фронтенду, які базуються на Vite — у всьому цьому немає сумнівів. Проте він залишається додатковою залежністю із власною конфігурацією, яка може відхилятися від версії Node, яка насправді використовується, та спричиняти проблеми, що забирають цілий день. Якщо кодова база фронтенду вже побудована навколо Vite, вибір Vitest все ще має сенс, оскільки поки що немає нічого корисного, що могло б замінити тестування компонентів у стилі jsdom. Однак для бекенд-сервісу чи інструменту командного рядка без використання бандлера використання Vitest лише заради кращої синтаксису тверджень втратило свою привабливість, коли node:test скоротив різницю у можливостях мокінгу та вимірювання покриття. Це інструмент, призначений для певного типу проектів, а не повна заміна для всього.

Це не заклик негайно переписувати всі існуючі кодові бази. Проте у майбутньому більше немає чіткої стандартної причини використовувати зовнішній інструмент для тестування під час створення нового сервісу Node — твердження, яке рік тому не було б актуальним. Екосистема витратила майже десять років на створення складних інструментів для заповнення прогалин, залишених самим середовищем виконання, і тепер, коли деякі з цих прогалин закрито, частина цих інструментів стала зайвим тягарем. Це не весь набір інструментів, слід зазначити. Лише більша частина, ніж очікувалося.

Пов’язана література

  • Порівняння агентів Frontier AI: Astra, Flash, Fable та Mythos — детальний аналіз продуктивності найновіших версій моделей GPT, Gemini та Claude у реальних завданнях, що вимагають використання агентів, таких як програмування, перегляд веб-контенту та робота з інструментами, а не лише у тестових сценаріях.
  • Налаштування нових функцій контролю чанкінгу Turbopack у Next.js 16.3 — практичний огляд нової конфігурації turbopackChunking у Next.js 16.3 із поясненням того, як параметри maxChunkCountPerGroup та generateComponentChunks впливають на розмір пакету та процес кешування.
  • Переписування TypeScript 7 на Go: що це означає для безпеки типів у React — Дізнайтеся, як компілятор TypeScript 7 на основі Go прискорює процес будування проектів та покращує інференцію генеричних типів, усуваючи приховані типи any у хуках React та JSX.
  • Всередині інтерцепторів NestJS: усунення проблеми затримки 96% у великих масштабах — Дізнайтеся, як конвеєр виконання AOP у NestJS та проблеми під час розбирання структур з RxJS спричинили стрімке зростання затримки P99, та як створити інтерцептор для аудиту без використання додаткової пам’яті для її усунення.