Библиотеки для тестирования Node.js для API, воркеров и кода базы данных
Библиотеки для тестирования Node.js могут покрывать API-запросы, воркеры, fake timers и очистку базы данных. В этом руководстве сравниваются пакеты и простые варианты настройки.

Почему все быстро превращается в хаос
Node.js-сервис редко делает только что-то одно. Один запрос может проверить валидацию маршрута, вызвать воркер, записать данные в базу и отправить задачу в очередь. Если тест проверяет только HTTP-ответ, он может не заметить именно ту часть, которая потом ломается.
Поэтому команды часто используют сразу несколько инструментов. Один пакет помогает поднять приложение и отправлять запросы. Другой дает более удобные HTTP-ассерты. Третий работает с fake timers, а еще один очищает тестовые данные. Такая смесь — это нормально, но она же добавляет больше настройки, больше общего состояния и больше способов, которыми тесты могут мешать друг другу.
Время добавляет еще один уровень сложности. Воркеры, ретраи, отложенные задачи и расписанные задания зависят от тайминга. Тест может проходить на вашем ноутбуке, а в CI падать, потому что ретрай сработал чуть раньше, промис завершился уже после конца теста или фейковые часы не совпали с реальным async-поведением.
Чаще всего доверие ломается именно на базе данных. Медленная очистка оставляет строки после себя, и следующий тест стартует уже с грязными данными. После этого ошибки выглядят случайными. Один запуск зеленый, в следующий раз вылезает сообщение, что пользователь уже существует, или задача в очереди выполняется дважды, потому что прошлый тест так и не удалил свои записи.
Простой сценарий регистрации хорошо показывает проблему. Вы отправляете один POST-запрос, приложение сохраняет пользователя, записывает аудитную строку и ставит в очередь письмо с приветствием. Чтобы нормально протестировать такой поток, нужны помощники для запросов, управление очередью, очистка базы и контроль часов. Очень немногие Node.js testing libraries покрывают все это в одном месте.
Когда изоляция начинает проседать, CI начинает врать. Общие порты, повторно используемые базы, утекшие таймеры и неочищенные моки создают ошибки, которые исчезают при повторном запуске. Обычно это значит, что тестовый стек слишком запутан, а не что у CI просто плохой день.
Что решить до установки чего-либо
Большинство тестовых стеков становятся грязными потому, что люди ставят пять пакетов еще до того, как решили, как именно должны запускаться тесты. В итоге появляются пересекающиеся инструменты, смешанные подходы и настройка, к которой через месяц никто не хочет прикасаться.
Начните с одного основного раннера. Выберите инструмент, который будет запускать тесты, следить за файлами, показывать ошибки и работать с файлами инициализации. Если потом менять это решение, часто приходится переделывать больше, чем ожидаешь.
Потом решите, когда тесты должны ходить в реальные сервисы. Для API это может означать реальную базу данных, но фейковый сервис отправки писем. Для воркера — реальную очередь Redis в интеграционных тестах и заглушку в unit-тестах. Этот выбор лучше сделать заранее, потому что он влияет на фикстуры, env-файлы и CI-задачи.
С самого начала держите unit- и integration-тесты отдельно. Разложите их по разным папкам, дайте им разные команды и установите разные ожидания по скорости. Unit-тест должен быть быстрым и узким. Интеграционный тест может идти дольше, но он должен доказывать, что реальные части работают вместе.
Небольшому сервису обычно хватает такого разделения:
- unit-тесты для чистых функций и небольших модулей
- интеграционные тесты для маршрутов, воркеров и потоков с базой данных
- совсем немного end-to-end проверок для самых рискованных путей
Добавляйте помощники только тогда, когда можете назвать дыру, которую они закрывают. Если ваш раннер уже умеет моки, возможно, отдельный пакет для моков не нужен. Если встроенных таймеров хватает, не добавляйте библиотеку для времени. Если очистка занимает три строки в общем helper-е, сначала сделайте так, прежде чем ставить под это отдельный пакет.
Перед тем как добавлять что-либо в стек Node.js testing libraries, задайте себе четыре простых вопроса: убирает ли это повторяющийся код, делает ли ошибки понятнее, хорошо ли работает в CI и сможет ли следующий разработчик быстро это понять. Если ответ «нет», не добавляйте это.
Раннеры и инструменты для ассертов
Ваш тестовый раннер задает тон всему остальному. Он определяет, как запускать тесты, как мокать модули, как управлять временем, как следить за файлами и как читать ошибки. Многие решения по Node.js testing libraries становятся проще, когда этот вопрос уже закрыт.
Vitest — сильный выбор по умолчанию для новых Node.js-проектов. Он быстрый, хорошо работает с TypeScript и современными настройками модулей и включает моки, spy и fake timers из коробки. Это значит меньше лишних пакетов и меньше расхождений в настройке между тестами API и тестами воркеров.
Jest тоже остается хорошим вариантом для многих команд. Он существует давно, с ним уже сталкивались большинство разработчиков, и на нем построено огромное количество существующего тестового кода. Если ваш репозиторий уже использует Jest, часто практичнее оставить его. Переписывать тестовую инфраструктуру редко окупается, если текущая схема не тормозит людей явно.
В Node есть еще node:test, и это важнее, чем кажется. Для небольшого сервиса или внутреннего инструмента встроенного раннера может быть достаточно. Вы получаете простой API для тестов без добавления еще одной зависимости, а это удобно, когда хочется держать проект компактным.
Компромисс — в удобстве. node:test закрывает базовые задачи, но вам могут не хватать дополнительных помощников, которые Vitest или Jest дают сразу.
Проще всего думать об этом так:
- Выбирайте Vitest для нового сервиса, если вам важны скорость и встроенные моки и таймеры.
- Выбирайте Jest, если команда уже его знает или проект уже от него зависит.
- Выбирайте
node:test, если кодовая база небольшая и вы хотите меньше движущихся частей. - Добавляйте Chai только если вам нужна отдельная библиотека ассертов в стиле, который команда уже любит.
У Chai по-прежнему есть свое место, особенно вместе с Mocha или node:test. Его ассерты читаются приятно, и некоторым командам такой стиль нравится больше, чем матчеры, привязанные к конкретному раннеру. Но если ваш раннер и так дает понятные ассерты, добавление Chai может ощущаться как еще одна лишняя библиотека.
Для небольшого API с одним воркером и базой данных я бы обычно не усложнял: одного Vitest хватает большинству команд. Чем меньше слоев вы навешиваете в начале, тем меньше сопровождения создаете потом.
Помощники для HTTP и API-тестов
Для интеграционных API-тестов лучший инструмент зависит от одной простой детали: вы вызываете приложение напрямую или общаетесь с реальным сервером по HTTP? От этого зависит, что именно тест сможет поймать.
Supertest хорошо подходит, когда приложение живет в Express или Fastify, а вам нужен быстрый отклик. Вы передаете объект приложения в тест, отправляете запрос и проверяете статус, заголовки и тело без открытия реального порта. Это ускоряет тесты и уменьшает объем кода для настройки.
Небольшой пример помогает понять идею. Если маршрут логина должен возвращать 401 при неверном пароле, Supertest позволяет обратиться к этому маршруту всего в несколько строк и чисто проверить ответ. Для многих команд это первый пакет, который они добавляют для HTTP-assertions for Node.js.
Pactum хорошо работает, когда сценарий становится длиннее. Его стиль легче читать, если нужно создать пользователя, сохранить токен, вызвать другой эндпоинт и сравнить часть JSON-ответа. Ассерты остаются короткими, поэтому файл с тестом не превращается в стену вложенных проверок.
Nock решает другую задачу. Ваш API может во время запроса обращаться к Stripe, Slack или другому сервису, и вы не хотите, чтобы тесты случайно выходили в реальную сеть. Nock может блокировать исходящий HTTP и возвращать ожидаемый ответ, что делает ошибки понятнее и защищает тесты от сбоев из-за медленного стороннего песочника.
Используйте Undici fetch, когда хотите проверить живой локальный сервер, а не сам объект приложения. Такой путь ловит больше реального поведения: биндинг порта, порядок middleware, cookies и настройки proxy. Он чуть медленнее, зато часто находит ошибки, которые in-process тесты пропускают.
Для небольших сервисов хорошо работает практичное разделение: Supertest — для большинства проверок маршрутов, Pactum — для длинных API-потоков, Nock — для исходящих вызовов, а Undici fetch — для нескольких end-to-end проверок на локальном сервере.
Воркеры, очереди и контроль времени
Среди Node.js testing libraries именно инструменты для управления временем сильнее всего экономят нервы в тестах воркеров. Ретрай, который в продакшене ждет 30 секунд, не должен заставлять тестовый набор ждать те же 30 секунд.
@sinonjs/fake-timers дает прямой контроль над Date, setTimeout, setInterval и callback-ами в очереди. Вы можете сдвинуть часы вперед одним шагом и проверить, действительно ли сработал ретрай, отложенная задача или запланированная очистка. Это делает fake timers в Node.js простым решением для логики backoff и работы по расписанию.
У Vitest есть встроенные fake timers, и они покрывают большинство случаев, которые сервисы используют каждый день. Если проект уже работает на Vitest, это обычно самый простой путь. Jest fake timers закрывают ту же задачу для команд, которые уже используют Jest, поэтому отдельно добавлять еще один таймерный инструмент обычно нет смысла, если только вам не нужен более тонкий контроль.
Контроль времени не решает все. Воркеры часто завершают работу «когда-нибудь потом», и точный момент может меняться от запуска к запуску. wait-for-expect помогает именно в таких тестах. Он продолжает проверять условие, пока оно не выполнится или не истечет таймаут, что удобно для сценариев вроде изменения статуса задачи на «done» или перехода счетчика ретраев с 0 на 1.
Некоторые ошибки проявляются только с реальным брокером. Мок не поймает неверную настройку Redis, сломанную кодировку сообщения или воркер, который никогда не подтверждает задачу. Testcontainers — хороший выбор, если вам нужна такая дополнительная уверенность без постоянного поднятия общей инфраструктуры. Он может запускать Redis или RabbitMQ специально для теста, а потом удалять их после завершения прогона.
Простое разделение обычно работает так:
- Используйте fake timers для задержек, ретраев и задач по расписанию.
- Используйте
wait-for-expectдля фоновой работы, которая завершается сама. - Используйте Testcontainers, когда поведение очереди зависит от Redis или RabbitMQ.
- Сначала используйте встроенные таймеры раннера, а к
@sinonjs/fake-timersпереходите только если нужен более тонкий контроль.
Небольшой пример хорошо показывает компромисс. Допустим, воркер повторяет неудачную отправку письма через 60 секунд. С fake timers тест заканчивается меньше чем за секунду. Затем один контейнерный тест подтверждает, что сообщение действительно проходит через Redis. Такое сочетание ловит большинство ошибок воркеров, не превращая набор тестов в медленный хаос.
Настройка и очистка базы данных
Тесты с базой данных обычно ломаются по скучным причинам. Один тест оставляет строки, другой ожидает пустую таблицу, и набор начинает проходить только на одной машине. Решение несложное. Начинайте каждый запуск с известного состояния и убирайте за собой после каждого теста или каждого файла с тестами.
Если вам нужно поведение, похожее на production, Testcontainers — один из самых надежных вариантов. Он может поднимать настоящие Postgres, MySQL или Redis для ваших тестов. Это дольше, чем in-memory-решение, но зато он ловит такие баги, которые моки скрывают: реальное поведение SQL, особенности транзакций и проблемы с подключением.
pg-mem заметно легче для простого кода на Postgres. Он работает в памяти, поэтому тесты заканчиваются быстро. Я бы использовал его для небольших проверок query builder-ов или функций доступа к данным, но не для случаев, где важны точное поведение Postgres, расширения или производительность.
Для кода на MongoDB ту же роль играет mongodb-memory-server. Он создает временную базу на время прогона, что упрощает настройку и делает очистку простой. Этого часто хватает сервисам, которые в основном читают, записывают и проверяют структуру документов.
Сброс схемы важен не меньше самой базы. Если команда уже использует Knex или Umzug, пусть миграции пересоздают схему перед запуском. Так вы тестируете ту же структуру, что использует приложение, и не получаете расхождение из-за ручного SQL.
Если в проекте используется Prisma, Prisma migrate reset обычно самый чистый путь. Он очищает базу, применяет миграции заново и дает каждому запуску одинаковую стартовую точку. Это может быть медленнее, чем просто очищать таблицы, зато снимает много неопределенности.
Практичное разделение выглядит так:
- Используйте Testcontainers для полных интеграционных тестов.
- Используйте pg-mem для быстрых проверок Postgres на уровне unit-тестов.
- Используйте mongodb-memory-server для изолированных Mongo-тестов.
- Используйте миграции Knex, Umzug или Prisma для восстановления состояния.
Небольшой сервис с Postgres и Redis может запускать pg-mem-тесты на каждое сохранение файла, а Testcontainers — в CI перед merge. Такой подход сохраняет быстрый локальный отклик, не жертвуя реальным покрытием базы данных там, где это важно.
Один реалистичный стек для небольшого сервиса
Небольшому сервису не нужен огромный набор Node.js testing libraries. Для Express API с Postgres и воркером на BullMQ обычно лучше работает более легкая схема. Вам нужна одна команда запуска, одна тестовая база, один Redis и никакой лишней обвязки, если она не экономит реальное время.
Практичный стек выглядит так:
- Vitest запускает весь набор и умеет моки, spy и fake timers.
- Supertest вызывает маршруты Express внутри процесса, так что публичный порт открывать не нужно.
- Testcontainers поднимает свежие Postgres и Redis только на время тестового прогона.
- BullMQ использует этот Redis, поэтому тесты воркеров совпадают с реальным поведением.
- Небольшой helper для очистки сбрасывает таблицы между тестами вместо ручного редактирования фикстур.
Представьте простой поток. Клиент отправляет POST /reports, API пишет строку в Postgres, а потом добавляет задачу BullMQ. Supertest может проверить статус маршрута, тело ответа и ошибки валидации. После этого тест может прочитать базу и убедиться, что строка появилась, а затем посмотреть очередь или запустить воркер и проверить побочный эффект.
Время — это то место, где тесты чаще всего становятся медленными. Если воркер повторяет попытку через 30 секунд или ждет 5 минут перед следующим заданием, реальное ожидание сильно тормозит весь набор. Fake timers в Vitest позволяют сдвинуть часы вперед несколькими строками, чтобы проверить ретраи, backoff и отложенные задачи без ожидания.
Такой стек еще и легко поддерживать в CI. Каждый запуск получает чистые контейнеры Postgres и Redis, поэтому тесты падают только по настоящим причинам, а не из-за старого локального состояния. Для небольшой команды это обычно важнее, чем добавлять еще три инструмента, которыми вы, возможно, никогда не воспользуетесь.
Как собрать стек вместе
Для небольшого Node.js-сервиса самый чистый тестовый поток — скучный по задумке. Один раз поднять реальные зависимости, один раз подготовить схему и держать каждый тест узким. Так вы получаете API-интеграционные тесты, которые ловят ошибки связки, но не превращают набор в медленный хаос.
Хорошо работает такой порядок:
- В global setup поднимите контейнеры, которые нужны приложению, чаще всего PostgreSQL и Redis, с помощью инструмента вроде Testcontainers.
- Перед стартом набора выполните миграции, чтобы каждый тест использовал ту же схему, что и приложение.
- В каждом тесте вставляйте только те строки, которые нужны конкретному сценарию.
- Вызывайте API через helper вроде Supertest, проверяйте HTTP-ответ, а потом читайте базу и сверяйте сохраненные строки.
- В конце очищайте очереди, останавливайте воркеры, закрывайте database pools и выключайте сервер.
Этот средний шаг важнее, чем кажется. Если тест засеивает двадцать таблиц «на всякий случай», никто уже не понимает, что именно важно. Лучший тест создает одного пользователя, один заказ или одну задачу в очереди, делает запрос и проверяет точный эффект. Если эндпоинт должен создать payment-строку и поставить follow-up задачу, проверьте оба действия. Не останавливайтесь на ответе 200.
С воркерами нужна та же дисциплина. Оставьте Redis или бэкенд очереди реальными, но подменяйте время только тогда, когда ожидание ретрая или запланированной работы сделает тест слишком медленным. Сдвиньте часы, запустите воркер и проверьте новую строку или изменение статуса.
Больше всего зависающих тестовых наборов появляется не из-за сложных багов, а из-за открытых handle-ов. Забытое подключение к очереди, незакрытый HTTP-server или живой таймер могут держать раннер открытым несколько минут. Соберите очистку в одном месте, запускайте ее всегда, и весь стек станет предсказуемым.
Ошибки, на которые уходит много часов
Даже хорошие Node.js testing libraries не спасают плохую тестовую схему. Больше всего времени обычно уходит на несколько привычек, которые в первый день выглядят безобидно, а потом превращают набор тестов в лотерею.
Классическая проблема — одна общая база данных для параллельных тестов. Тест A вставляет пользователя, тест B ожидает пустую таблицу, и оба проходят или падают в зависимости от тайминга. Используйте отдельную базу, схему или транзакционную границу для каждого worker-а. Если это кажется тяжелым, сначала уменьшите параллелизм, прежде чем доверять результатам.
Еще один источник потерь — когда вы мокаете приложение там, где хотели протестировать само приложение. Если ваш «интеграционный» тест подменяет router, auth-слой или database client, вы уже не проверяете реальный путь. Такой тест часто горит зеленым, а настоящий сервис по-прежнему возвращает неправильный статус-код или неправильную форму ответа.
Fake timers тоже могут ломать тесты незаметно. Они хорошо работают для ретраев, задержек и задач по расписанию. Но плохо сочетаются с тестами, которые одновременно ждут реальный сетевой I/O, настоящую базу данных или queue client с собственной внутренней тайминг-логикой. Тест webhook-а, который использует fake timers и потом ждет реальный HTTP callback, может зависнуть на минуты.
Воркеры и очереди тоже требуют очистки. Если воркер продолжает опрашивать очередь после проверки, процесс теста может вообще не завершиться, либо следующий тест подхватит старые задачи. Закрывайте воркер, останавливайте интервалы, опустошайте очередь и закрывайте Redis или database pools каждый раз.
Большие seed-файлы — еще один медленный способ потерять время. Если тест проверяет одно правило доступа, загрузка 5000 строк — это просто шум. Создавайте две или три записи, которые действительно нужны. Маленькие фикстуры легче читать, быстрее запускать и проще отлаживать, когда что-то ломается.
Быстрые проверки перед коммитом
Тестовый стек может казаться нормальным в первый день, а уже через три недели начать раздражать. Большинство Node.js testing libraries дешево добавить, но каждая приносит конфиг, крайние случаи и еще одну вещь, которую нужно дебажить в CI.
- Спросите, нужна ли этому тесту реальная база данных. Если код строит SQL, зависит от индексов или использует транзакции, моки могут скрыть баг. Если тест проверяет только валидацию входных данных или небольшой ответвленный сценарий, реальная база часто просто тратит время.
- Проверьте, может ли один раннер покрыть большую часть набора. Если Vitest или Jest уже дают вам ассерты, моки, fake timers, watch mode и coverage, второй раннер обычно добавляет больше настройки, чем пользы.
- Перед коммитом проверьте поддержку ESM и TypeScript. Пакет, который работает только через особые loader-ы, странные флаги или хрупкие import-трюки, будет раздражать команду при каждом обновлении Node.
- Убедитесь, что CI может запускать набор так же, как ноутбуки разработчиков. Если инструменту нужны нестандартные shell-хаки, медленные bootstrap-скрипты или необычные контейнеры, за этим почти всегда идут flaky builds.
- Подумайте о том, что будет через полгода. Новые коллеги должны быстро понимать, как все устроено, и знать, как заменить или исправить пакет, не продираясь через лабиринт документации.
Небольшой пример помогает. Допустим, у сервиса один API, один воркер и PostgreSQL. Обычно хватает скучной схемы: один раннер, один инструмент для HTTP assertions for Node.js, встроенные fake timers и один понятный способ для очистки базы данных в тестах.
Такому стеку проще доверять. Когда тест падает, команда может понять, в приложении ошибка, в базе данных или в очереди, вместо того чтобы гадать, какой именно слой тестовых инструментов сломался первым.
Что делать дальше
Выберите один путь через приложение и сначала протестируйте именно его. Хорошая стартовая точка — один API-поток, один поток воркера и одно правило очистки базы. Этого достаточно, чтобы проверить, что настройка работает, и не превратить тестовый набор в отдельный проект.
Например, протестируйте запрос на регистрацию, затем проверьте, что приложение ставит в очередь задачу на приветственное письмо, а потом сбросьте базу, чтобы следующий запуск начался с чистого состояния. Если этот поток кажется болезненным, ваш стек слишком тяжелый.
- Добавьте один интеграционный тест API с HTTP-assertions.
- Добавьте один тест воркера с fake timers или управляемыми часами.
- Добавьте одно правило сброса базы, например откат транзакции или очистку таблиц.
Держите локальные прогоны быстрыми. Разработчики перестают запускать тесты, если базовая проверка занимает минуты. Более медленные сценарии оставьте для CI, где можно позволить себе полную загрузку базы, более широкое покрытие и несколько долгих тестов воркеров.
Запишите, что делает каждый helper. Достаточно короткой заметки в репозитории: какой пакет запускает тесты, какой отправляет HTTP-запросы, какой управляет временем и какой чистит данные. Хорошие Node.js testing libraries помогают, но понятные правила помогают еще больше, когда в команду приходит новый человек или когда вы возвращаетесь к набору через месяц.
Если вы работаете в небольшом стартапе, держите настройку скучной и недорогой. Один раннер, один HTTP-helper, один способ очистки и только те моки, которые действительно нужны, обычно лучше целой груды инструментов.
Если вашей команде нужен lean test setup или ревью Fractional CTO, Oleg на oleg.is поможет собрать практичный план. Это особенно важно, когда API, воркеры и код базы данных уже существуют, а вам нужен набор тестов, который люди действительно будут запускать.