Статья опубликована на английском языке.
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.
Запрос к пул-репозиторию назывался «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.mock('./path') и его механизмом поднятия кода. Функции mock.method и mock.module в Node обрабатывают большую часть таких задач, но их использование требует более явного указания того, что именно заменяется и в какой момент.
В проекте, ориентированном на компоненты React с обширными тестами интерфейса, основанными на снимках состояния, ожидайте, что этот процесс перехода будет значительно более трудным, чем при использовании бэкенд-сервиса, применявшегося в ходе данной оценки. Замена работает практически бесшовно в настоящее время только в коде серверной части и инструментах командной строки.
Параллельная обработка — ещё один фактор, который стоит учесть перед применением этого подхода к крупному набору тестов. Архитектура пула рабочих процессов Jest не распределяет файлы тестов между процессами так же, как встроенный запускатель Node, и в зависимости от структуры вашего набора тестов общее время выполнения может измениться в нежелательном направлении в крупном монорепозитории, даже если отдельные файлы тестов выполняются быстрее в изоляции. Стоит измерить это на реальных машинах, используемых в вашей CI-пайплайне, а не на локальном ноутбуке с несколькими свободными ядрами и без конкуренции за ресурсы.
Как на самом деле происходила миграция
Для тех, кто рассматривает такой переход, вот примерно как это происходило: оба инструмента тестирования работали параллельно в системе CI примерно неделю, вместо того чтобы сразу заменить их все в одном запросе на пул-реквест. Те же файлы тестов проходили обработку в двух отдельных задачах CI, причем результаты и время выполнения сравнивались напрямую. В ходе этого процесса были обнаружены два теста, которые незаметно использовали глобальные переменные специфичные для Jest, введение которых никто не помнил; оба проблемных момента были устранены в течение часа после их обнаружения. Лишь когда результаты двух задач CI стабильно совпадали в течение целой недели, был отправлен пул-реквест с удалением Jest. Это медленный и не очень привлекательный способ решения задачи, но когда речь идет о механизме безопасности, предназначенном для выявления ошибок в других частях кода, скучный, но возможный к откату процесс лучше, чем быстрый, но необратимый.
А что насчет Vitest?
Эту проблему стоит решить напрямую, ведь это первый вариант, о котором обычно говорят. Vitest действительно работает быстрее Jest, предлагает более удобный API и легко интегрируется в рабочие процессы фронтенда, управляемые Vite — в этом нет никаких сомнений. Тем не менее это дополнительная зависимость с собственной настройкой, которая может отличаться от версии Node, используемой на самом деле, и приводить к сбоям, занимающим целое утро. Если кодовая база фронтенда уже построена на Vite, выбор Vitest остается разумным, поскольку пока нет ничего корневого, что могло бы заменить тестирование компонентов в стиле jsdom. Однако для бэкенд-сервиса или инструмента командной строки без бандлера использование Vitest только ради более удобной синтаксиса утверждений потеряло привлекательность, когда node:test сократил разрыв в возможностях мокинга и анализа покрытия. Это инструмент, подходящий для определенного типа проектов, а не полная замена всему.
Настройка режима реального времени.
Что это означает
Это не призыв сразу же переписать весь существующий кодовый базис. Однако в будущем больше нет явной стандартной причины использовать внешний инструмент для тестирования при запуске нового сервиса на Node, что год назад было бы неактуально. Экосистема потратила почти десять лет на создание сложных инструментов для устранения пробелов, оставленных самим средством выполнения, и теперь, когда некоторые из этих пробелов устранены, часть этих инструментов становится ненужным бременем. Речь не идет обо всей инструментальной сборке, а лишь о более значительной части, чем ожидалось.
Связанные материалы
- Роль моста TypeScript 6 на пути к нативному компилятору TS 7 — Узнайте, как TypeScript 6 обновляет стандартные настройки, механизмы разрешения модулей и синтаксис импорта для подготовки кодовых баз к более быстрому компилятору TypeScript 7, основанному на языке Go.
- Миграция API Express в обработчики маршрутов Next.js App Router — Узнайте, как преобразовать маршруты Express, промежуточные компоненты и шаблоны обработки данных в структуру Next.js App Router с использованием серверных компонентов, а также о соображениях по развертыванию.
any в хуках React и JSX.