Тестирование и эксплуатация

Урок о двух опорах надёжной эксплуатации: автотестах, которые ловят регрессии до прода, и менеджере процессов, который держит сервис живым.

Менеджер процессов — это программа, которая запускает ваш сервис, перезапускает его при падении и следит за его здоровьем, освобождая вас от ручного node server.js в терминале.

Код без тестов страшно менять: любая правка может тихо что-то сломать. А сервис без менеджера процессов умирает при первом сбое и не поднимается после перезагрузки сервера. Этот урок закрывает обе темы: сначала — что и чем тестировать, затем — как стабильно крутить приложение в проде через pm2 или systemd, масштабировать по ядрам и следить за его состоянием.

Зачем это нужно на практике

Тесты — это страховка скорости: с ними вы рефакторите и выкатываете уверенно, потому что регрессию поймает CI, а не пользователь. Менеджер процессов — страховка доступности: процесс упал в 3 ночи — он сам поднялся, сервер перезагрузился — сервис стартовал автоматически. Вместе они превращают «работает на моей машине» в «работает в проде без няньки».

node:test и Jest

В современном Node есть встроенный тест-раннер node:test — без установки зависимостей. Этого достаточно для большинства проектов.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sum } from './math.js';

test('sum складывает два числа', () => {
  assert.equal(sum(2, 3), 5);
});

test('sum бросает на нечисловой ввод', () => {
  assert.throws(() => sum(2, 'x'));
});

Запуск — node --test. Альтернатива — Jest: богатая экосистема, моки, снапшоты, удобен для больших проектов и фронтенда. Выбор между ними — вопрос масштаба и привычек команды; принципы тестирования одинаковы.

# встроенный раннер: найдёт и прогонит *.test.js
node --test

# Jest
npx jest

Что тестировать

Тестируют не «всё подряд», а то, что важно и ломается. Полезно держать в голове «пирамиду тестов»:

  • Unit-тесты (много). Чистая логика: расчёты, парсинг, валидация. Быстрые, без сети и БД.
  • Интеграционные (меньше). Связки: маршрут + БД, сервис + очередь. Проверяют, что части стыкуются.
  • E2E (мало). Сценарий целиком через HTTP. Дорогие и медленные, поэтому их немного.

Фокус — на бизнес-правилах и граничных случаях: пустой ввод, отрицательные числа, ошибка БД, повторный запрос. Не тратьте силы на тесты тривиальных геттеров — цельтесь в код, где есть логика и риск ошибки.

// интеграционный тест маршрута через supertest
import request from 'supertest';
import { app } from './app.js';

test('POST /users отклоняет невалидный email', async () => {
  const res = await request(app)
    .post('/users')
    .send({ email: 'not-an-email', age: 30 });
  assert.equal(res.status, 400);
});

Менеджер процессов: pm2 и systemd

Запускать прод-сервис как node server.js в терминале нельзя: закрылась сессия — упал сервис. Нужен менеджер. pm2 — популярный выбор для Node: рестарт при падении, автозапуск, логи, кластер.

# запустить под именем
pm2 start server.js --name api

# список процессов и их состояние
pm2 list

# логи и статус
pm2 logs api

# сохранить набор процессов и включить автозапуск при загрузке ОС
pm2 save
pm2 startup

Альтернатива без лишних зависимостей — systemd, штатный init в Linux. Он тоже умеет рестарт и автозапуск, а заодно — изоляцию и лимиты ресурсов.

[Unit]
Description=Node API
After=network.target

[Service]
User=node
WorkingDirectory=/app
ExecStart=/usr/bin/node server.js
Restart=on-failure
Environment=NODE_ENV=production
Environment=PORT=3000

[Install]
WantedBy=multi-user.target

Здесь Restart=on-failure поднимает сервис после падения, User=node — запуск без root (см. урок о безопасности), а WantedBy=multi-user.target включает старт при загрузке системы.

Рестарт и кластеризация

Один Node-процесс использует одно ядро CPU. На многоядерном сервере это расточительно. Решение — кластеризация: запустить несколько одинаковых процессов (по числу ядер) за общим портом; ОС/менеджер распределяет соединения между ними. Это и масштабирование, и отказоустойчивость — упал один воркер, остальные держат нагрузку.

# pm2: запустить по процессу на каждое ядро
pm2 start server.js -i max --name api

# плавный перезапуск без простоя при деплое
pm2 reload api

Команда pm2 reload важна для деплоя без даунтайма: pm2 поднимает новые воркеры и гасит старые по одному, поэтому сервис не «мигает». Чтобы это работало чисто, приложение должно корректно обрабатывать SIGTERM (graceful shutdown из первого урока) — иначе при перезапуске вы оборвёте текущие запросы.

Мониторинг здоровья

Менеджер перезапускает упавший процесс, но «процесс жив» ≠ «сервис здоров»: он может висеть, не отвечая. Поэтому добавляют health-эндпоинт — лёгкий маршрут, который проверяет ключевые зависимости и отвечает быстро.

app.get('/healthz', async (req, res) => {
  try {
    await db.query('SELECT 1'); // БД доступна?
    res.status(200).json({ status: 'ok' });
  } catch (err) {
    res.status(503).json({ status: 'unhealthy' });
  }
});

Этот маршрут опрашивает балансировщик или оркестратор: ответил 200 — шлём трафик, ответил 503 или молчит — выводим из ротации. Дополняйте картину метриками (latency, частота ошибок, память) и алёртами, чтобы узнавать о проблеме раньше пользователей. pm2 показывает базовое здоровье через pm2 monit.

Как это работает под капотом

Кластеризация в Node опирается на модуль cluster: главный процесс открывает слушающий сокет и через механизм ОС передаёт файловый дескриптор воркерам, поэтому несколько процессов делят один порт, а ядро раздаёт входящие соединения между ними. pm2 в режиме -i — это обёртка над тем же механизмом. Менеджер процессов отслеживает PID дочернего процесса: когда тот завершается с ненулевым кодом, ОС уведомляет родителя (через сигнал SIGCHLD), и менеджер по своей политике (Restart=on-failure у systemd) поднимает процесс заново. Автозапуск при загрузке держится на том, что и pm2 (pm2 startup), и systemd регистрируют сервис как unit, который init-система стартует на нужном этапе загрузки. Health-эндпоинт — обычный HTTP-маршрут; «магия» лишь в том, что внешний опрашиватель трактует код ответа как сигнал «годен/негоден».

Частые ошибки

  • Прод как node server.js в терминале. Закрылась сессия или упал процесс — сервиса нет, автозапуска нет.
  • Тесты «для галочки». Покрыты тривиальные геттеры, а бизнес-логика и граничные случаи — нет.
  • Кластер без graceful shutdown. pm2 reload при деплое обрывает текущие запросы, если не обработан SIGTERM.
  • Health-чек, который всегда отвечает 200. Не проверяет зависимости — «здоров» даже когда БД лежит.
  • Сервис под root в unit-файле. Забыли User= — нарушили least privilege.
  • Нет метрик и алёртов. О проблеме узнаёте от пользователей, а не из мониторинга.

Итоги

  • Тестируйте через node:test или Jest; стройте пирамиду: много unit, меньше интеграционных, мало E2E.
  • Цельтесь в бизнес-логику и граничные случаи, не в тривиальный код.
  • В проде держите сервис под менеджером процессов (pm2 или systemd) с рестартом и автозапуском.
  • Кластеризуйте по числу ядер; для деплоя без простоя используйте pm2 reload вместе с graceful shutdown.
  • Добавьте health-эндпоинт, проверяющий зависимости, и дополните его метриками и алёртами.
Проверьте себя
1. Что отражает «пирамида тестов» применительно к Node-приложению?
AЧем выше тест в иерархии, тем он быстрее
BНужно писать только E2E-тесты, остальные не нужны
CМного быстрых unit-тестов в основании, меньше интеграционных, мало медленных E2E наверху
DВсе типы тестов должны быть в равной пропорции
2. Зачем нужен менеджер процессов (pm2 или systemd) для продакшен-сервиса на Node?
AОн ускоряет выполнение JavaScript-кода
BОн перезапускает упавший процесс и обеспечивает автозапуск при загрузке сервера
CОн заменяет тесты приложения
DОн шифрует трафик между клиентом и сервером
3. Почему health-эндпоинт должен проверять зависимости (например, делать SELECT 1 к БД), а не всегда возвращать 200?
AЧтобы эндпоинт работал быстрее
BПотому что «процесс жив» не равно «сервис здоров» — он может висеть с недоступной БД
CЭто требование протокола HTTP
DЧтобы уменьшить размер ответа