Node.js Internals

Testing Internals: Node.js Test Runner

В 2015 Google потратил **2 млн CPU-часов в день** на запуск тестов. Встроенный тест-раннер Node.js решает проблему по-другому: **ноль зависимостей, мгновенный старт, нативная интеграция с V8**. Никаких `npm install`, никаких конфликтов версий - просто `node --test`.

  • **Vercel** перешёл с Jest на `node:test` в 2023 - **сократил время CI в 2 раза** (не нужно устанавливать 80 MB зависимостей)
  • **Cloudflare Workers** использует встроенные тесты для edge-функций - они запускаются в изолированных V8 isolates, где нельзя использовать npm
  • **Deno** изначально встроил тест-раннер - Node.js последовал этому примеру, чтобы конкурировать

Встроенный тест-раннер Node.js

С Node.js 18 появился **встроенный тест-раннер** - `node:test`. Это стабильный API с нулевыми зависимостями для запуска тестов.

**Зачем нужен встроенный раннер?** Jest/Mocha добавляют 50+ зависимостей в `node_modules`. Встроенный раннер работает из коробки - никаких `npm install`, никаких конфликтов версий.

История появления

  • **Node.js 16.17.0** (2022) - первая экспериментальная версия `node:test`
  • **Node.js 18.0.0** (2022) - стабилизация API, добавление `describe/it`
  • **Node.js 20.0.0** (2023) - встроенное покрытие кода, `mock` API
  • **Node.js 22.0.0** (2024) - `--test-reporter`, параллельные тесты

Сравнение с внешними библиотеками

Параметрnode:testJestMocha + Chai
Зависимости0~50~20
Размер установки0 MB~80 MB~40 MB
Время установки0 сек~30 сек~15 сек
Скорость запускаМгновенно~1-2 сек~0.5 сек
Mocking APIВстроенВстроенТребует Sinon
CoverageV8 nativeIstanbulТребует nyc/c8

**Ограничения:** `node:test` не поддерживает snapshot-тестирование и автоматический mocking модулей (как `jest.mock`). Для этого нужны внешние библиотеки.

Почему встроенный тест-раннер быстрее стартует?

API тест-раннера: describe, it, hooks

API `node:test` напоминает Jest/Mocha, но с нюансами. Основные функции: `describe`, `it`, `test`, `before/after`, `beforeEach/afterEach`.

Базовая структура теста

Жизненный цикл тестов

**Порядок выполнения:** `before` → (`beforeEach` → `test` → `afterEach`) для каждого теста → `after`. Если тест упадёт, `afterEach` всё равно выполнится.

Модификаторы: only, skip, todo

Вложенные describe

**Разница с Jest:** В node:test нет автоматического hoisting для `beforeEach`. Они выполняются в порядке объявления. В Jest `beforeEach` всегда идут перед `it`, даже если написаны после.

Сколько раз выполнится `beforeEach` для 5 тестов?

Моки: функции, методы, таймеры, модули

**Mocking** - замена реальных зависимостей на контролируемые заглушки. Node.js предоставляет API для моков функций, методов объектов, таймеров и модулей.

mock.fn() - замена функций

mock.method() - замена методов объектов

mock.timers() - управление временем

Module mocking через register()

**Автоочистка моков:** В node:test все моки автоматически восстанавливаются после завершения теста. В Jest нужно вызывать `jest.clearAllMocks()` вручную.

**Ограничение:** `mock.module()` работает только с ES-модулями (`import/export`). Для CommonJS (`require`) нужно использовать `proxyquire` или аналоги.

Как проверить, что мок-функция была вызвана с определённым аргументом?

Покрытие кода: --experimental-test-coverage

**Code Coverage** - метрика, показывающая, какой процент кода был выполнен во время тестов. Node.js 20+ имеет встроенное покрытие через V8, без сторонних инструментов.

Запуск с покрытием

Типы покрытия

ТипОписаниеПример
**Line Coverage**% выполненных строк5 из 10 строк = 50%
**Function Coverage**% вызванных функций2 из 3 функций = 66%
**Branch Coverage**% пройденных веток if/elseif/else: только if = 50%
**Statement Coverage**% выполненных инструкцийОбычно ≈ Line Coverage

Интеграция с c8

**Разница между встроенным покрытием и c8:** Встроенное (`--experimental-test-coverage`) выводит только процент. c8 создаёт подробные HTML-отчёты с подсветкой непокрытых строк.

Пример вывода покрытия

**100% покрытие ≠ отсутствие багов.** Можно покрыть все строки, но не проверить граничные случаи (null, пустые массивы, переполнение).

Какой тип покрытия показывает, что выполнены обе ветки if/else?

Стратегии тестирования: Unit, Integration, E2E

**Стратегия тестирования** определяет, что и как тестировать. Основные уровни: **Unit** (изолированные функции), **Integration** (взаимодействие модулей), **E2E** (полный сценарий пользователя).

Test Pyramid - пирамида тестов

**Правило пирамиды:** Чем выше уровень, тем медленнее и дороже тесты. 70% unit-тестов дают быстрый фидбек при разработке. 10% E2E покрывают критические сценарии.

Unit тесты - изоляция зависимостей

Integration тесты - реальная БД

E2E тесты - полный сценарий

TDD Workflow (Test-Driven Development)

  1. **Red:** Написать тест, который падает (функция ещё не реализована)
  2. **Green:** Написать минимальный код, чтобы тест прошёл
  3. **Refactor:** Улучшить код, не ломая тесты

**TDD - не серебряная пуля.** Для прототипов и экспериментов TDD замедляет. Используй для критичных модулей (платежи, авторизация).

Нужно стремиться к 100% покрытию кода тестами

80% покрытия + тесты на критичные сценарии лучше, чем 100% формального покрытия

Последние 20% часто покрывают тривиальный код (геттеры, логгеры). Время лучше потратить на E2E-тесты критичных flow (регистрация, оплата). 100% покрытия не гарантирует отсутствие багов - важнее качество проверок, а не количество строк.

Почему unit-тестов должно быть больше, чем E2E?

Ключевые идеи

  • **node:test** (Node.js 18+) - тест-раннер с нулевыми зависимостями, API как у Jest/Mocha
  • **Моки:** `mock.fn()` для функций, `mock.method()` для объектов, `mock.timers` для setTimeout/setInterval
  • **Coverage:** Встроенное V8-покрытие через `--experimental-test-coverage`, c8 для HTML-отчётов
  • **Test Pyramid:** 70% unit (быстро), 20% integration (реальная БД), 10% E2E (полные сценарии)
  • **TDD workflow:** Red (упавший тест) → Green (минимальная реализация) → Refactor (улучшение кода)

Связанные темы

Тестирование пересекается с внутренностями Node.js:

  • V8 Isolates — node:test запускает каждый файл в отдельном контексте через vm.Module (как в Cloudflare Workers)
  • Module System — mock.module() использует import.meta.resolve и loader hooks для подмены модулей
  • Event Loop — mock.timers перехватывает C++ биндинги uv_timer_start для управления временем

Вопросы для размышления

  • Почему Google тратит миллионы CPU-часов на тесты? Какую проблему это решает в масштабе монорепозитория на 2 млрд строк кода?
  • В каких случаях 100% покрытие кода вредит проекту? Когда лучше написать один E2E-тест вместо 10 unit-тестов?
  • Как mock.timers реализован внутри? Что происходит с event loop'ом Node, когда вызываешь mock.timers.tick(1000)?

Связанные уроки

  • devops-09
Testing Internals: Node.js Test Runner

0

1

Войти