| От Jest к Vitest на backend тестах: как мы мигрировали тестовый фреймворк для ускорения CI и повышения стабильности |
| 19.01.2026 00:00 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Привет! Я Максим Кузьмин, старший инженер по автоматизации в команде Т-Путешествий. Строю и развиваю процессы автоматизации и разрабатываю инструменты тестирования. Для внутренних нужд мы разработали фреймворк для изолированного тестирования бэкенда. Он написан на TypeScript, обеспечивает гибкость, масштабируемость и интеграцию с разными внутренними системами. Выступает как единое решение для написания, запуска и поддержки тестов в стабильной и предсказуемой среде. В статье будет история миграции с Jest на Vitest. Расскажу, какие проблемы подтолкнули нас к переходу, как мы адаптировали окружение и какие результаты получили. Поделюсь опытом улучшения скорости запуска тестов и стабильности результатов. Надеюсь, что наш опыт поможет кому-то превратить автотесты из источника проблем в устойчивый инструмент контроля качества. Как проблемы backend-тестирования подтолкнули нас к созданию собственного инструмента В нашей команде бэкенд реализован на двух относительно редких стеках: крупный монорепозиторий на Haskell (10–15 сервисов) и около десятка микросервисов на Scala (часть — внутри монорепозитория, часть — в отдельных репозиториях). Использование не самых распространенных языков осложняло поддержку и развитие тестовой инфраструктуры. Разнородность кодовой базы дополнительно усложняла унификацию процессов. У нас было около 3600 сценариев автотестов, в основном end-to-end, второго слоя пирамиды почти не было. Упавшие автотесты хранились в отдельном репозитории и запускались вне основного GitLab CI разработки и не блокировали его. В CI/CD не было встроенного запуска автотестов, они не запускались автоматически при изменении. Е2Е-тесты требуют запуска в среде с поднятой инфраструктурой. Преобладание Е2Е, отсутствие автоматического запуска, внешнее расположение тестов и недостаточная интеграция в основной CI/CD-пайплайн разработки привели к тому, что полный прогон тестов занимал около дня. ![]() Еще хуже ситуация была со стабильностью тестов: успешны лишь около 40% проверок, и команде QA регулярно приходилось разбирать и актуализировать сотни падающих тестов. Причины падений были разные: то недоступен стенд сервиса получения тестового клиента, то не работает мокирование, потому что упал внешний мокер, то еще какие-то внутренние сервисы не отвечают. Со временем команда QA просто устала тратить столько сил из-за нестабильности тестовой среды. Мы пробовали разные подходы: запускали ночные пайплайны с полным запуском регресса, чтобы следить за состоянием системы между релизами, или при сборке конкретного сервиса настраивали дочерние пайплайны только с тестами этого сервиса. Но такие меры не решили проблему полностью: при больших релизах все равно запускался полный регресс, который мог идти двое суток. А при сбоях инфраструктуры общее время могло тянуться до нескольких дней. Проанализировали нашу работу и выделили ряд основных проблем. Сложная и разнородная технологическая база. Haskell + Scala в одном контуре затрудняют сопровождение и унификацию процессов. Внешнее хранение тестов. Расположение в отдельном репозитории мешает синхронизации с кодом. Отсутствие автоматического запуска в CI/CD. Тесты не блокируют пайплайн, баги могут свободно попасть в релиз. Долгий прогон регресса. От суток до двух-трех дней при сбоях инфраструктуры. Преобладание медленных Е2Е-тестов. Зависимость от стендов и работа только на них со всеми ограничениями. Низкая стабильность тестов. Около 40 % успешных прогонов, много «флаки». Высокие трудозатраты QA на поддержку. Разбор сотен упавших тестов, ручные перезапуски и расследования причин. Мы поняли, что так продолжаться не может. Релизы стали занимать по два дня, QA тратили дни на перепрохождение неудачных кейсов, а дух команды падал перед каждым запуском. Мы подошли к необходимости срочных изменений. Нужен был инструмент, который позволил бы быстро сдвинуть дело с мертвой точки — без масштабных перестроек и сложных внедрений, но с ощутимым результатом уже в ближайшей перспективе. При этом не было возможности обучать команды новым языкам ради кардинального пересмотра подхода к тестированию. Исходя из наших проблем мы сформулировали требования к новому инструменту тестирования:
Первый шаг: Jest и чистое окружениеВ качестве базы для нового фреймворка мы остановились на TypeScript. QA-инженеры уже использовали этот стек на фронтенде, а для бэкенд-разработки не нужны глубокие знания ЯП для поддержки и написания тестов. Выбор TypeScript казался наиболее практичным в нашем случае. Основной формат обмена данными — JSON, экосистема JS/TS обеспечивает для него наиболее естественную поддержку. А еще у нас накопились наработки для автоматизации и внутренние интеграции с фронта, которые можно было быстро адаптировать под новое решение. Так мы сократили трудозатраты на внедрение и получили ощутимый результат в короткие сроки. В первой итерации мы взяли Jest и построили на нем минимальный фреймворк, где окружение каждый раз полностью пересоздавалось через docker-compose с инициализацией в globalSetup (скрипте настройки среды, вызываемом из конфигурации до запуска тестов). Такой подход гарантировал «чистоту» окружения: никакие данные от предыдущих прогонов не оставались в контейнерах. В результате тесты стали вести себя предсказуемо — они стабилизировались и перестали произвольно падать. Проблема случайных flaky-тестов была решена. С самого начала мы понимали, что управлять множеством YAML-конфигураций для docker-compose сложно. Как только в нашей инфраструктуре появилась стабильная поддержка Testcontainers (библиотеки для интеграционного тестирования, которая позволяет программно запускать Docker-контейнеры без громоздких YAML-файлов), мы заменили docker-compose на программный API testcontainers-node. Теперь в globalSetup достаточно было нескольких строк кода: создать внутреннюю docker-сеть, поднять нужные контейнеры приложений, дождаться их готовности и автоматически удалять контейнеры после завершения теста. Конфигурация тестового окружения перешла из пугающей кучи YAML в лаконичный код. Это позволило полностью пересоздавать окружение при каждом запуске, что сделало процесс более надежным и предсказуемым. После стабилизации тестов появилась возможность использовать их для блокировки пайплайна — теперь падения реально отражали проблемы сервиса. Мы перенесли тесты в репозитории приложений рядом с кодом, перестроили CI-пайплайны и настроили так, чтобы при сборке каждого сервиса автоматически запускались только тесты, относящиеся к нему. Тесты стали надежным инструментом контроля качества, встроенным в процесс разработки. В итоге время полного прогона регресса сократилось и стало сравнимо со временем выполнения основного пайплайна.
Добившись стабильности и предсказуемости тестов за счет чистого, изолированного окружения, мы подумали о следующем узком месте — скорости прогонов. Мы переосмыслили выбор тест-раннера: решили найти решение, которое сохранит достигнутую надежность, но при этом улучшит производительность и повысит скорость тестирования в долгосрочной перспективе. Шаг второй: миграция на Vitest. Преимущества и задел на будущееДолгое время в проекте мы использовали Jest как тест-раннер, поскольку он был популярен и опробован командой ранее. Окружение для интеграционных тестов разворачивали через Testcontainers, что позволяло программно запускать необходимые контейнеры для сервисов. А летом 2024 года мы увидели упоминание Vitest в обзоре State of JS 2023 (фреймворк для модульного тестирования с фокусом на высокую производительность и гибкую настройку, предлагающий знакомый Jest-подобный API) и задумались, не стоит ли попробовать заменить Jest новым инструментом. Vitest позиционируется как современный фреймворк для модульного тестирования, оптимизированный для скорости и легкой настройки. У него нативная поддержка ESM (современный стандарт модулей ECMAScript, пришедший на смену классическому CommonJS) и TypeScript без дополнительных настроек. API Vitest совместим с Jest и хорошо знаком по предыдущему опыту. А еще у Vitest более гибкий lifecycle. Особенно привлекали возможности публичного низкоуровневого программного API, более эффективное использование ресурсов и тонкая настройка параллельных прогонов. Преимущества Vitest:
Мы решили заменить тест-раннер. Но сначала нужно было перевести проект на ESM (ES Modules). Vitest нативно работает с ESM, поэтому переход на него стал не столько подготовительной задачей, сколько первым этапом во всем процессе.
Переход на ESM Первым делом мы перевели кодовую базу нашего фреймворка на формат ECMAScript Modules. Это модульная система JavaScript, стандартизированная в ES6, в отличие от устаревшего CJS (CommonJS), который использовался изначально. Для этого в Мы заменили некоторые библиотеки, использующие CJS, на аналоги, поддерживающие ESM. Например, вместо Еще поправили импорты в коде: в ESM требуется указывать расширение для относительных путей, поэтому в таких импортах мы добавили Глобальные переменные CommonJS После правок мы получили стабильную ESM-версию фреймворка, с которой можно было запускать синтетические тесты. Мы сравнили результаты Jest и Vitest во время тестирования тестового приложения на 300 тестов по пяти запускам.
Vitest в режиме Выводы по первым тестам:
Оптимальная конфигурация на локальной машине давала Vitest преимущество и по скорости, и по экономии памяти. Исправления с публикацией ESM-пакетов. Сборкой проекта у нас управляет NX (инструмент для моно-репозиториев от Nrwl). Мы столкнулись с тем, что при билде пакета он продолжал публиковаться в CJS-формате. Оказалось, что в версиях После этого мы протестировали Vitest на клиентском приложении, обновили зависимости и провели тестирование в разных конфигурациях. Сравнение конфигураций Vitest в локальной средеНа локальной машине мы замерили производительность без какого-либо изменения тестов. Перебрали несколько вариантов конфигураций, но для наглядности остановимся на ключевых. В таблице собрали результаты для текущего набора тестов проекта — всего 1829 тест-кейсов. Время в секундах указано по среднему из нескольких запусков.
Результаты экспериментов: Наилучшее время — 215 секунд, но при этом было много падений: 199 из 300 тестов упали. Достигается при конфигурации: Лучший баланс между скоростью и стабильностью получился, когда за 269 секунд было всего 95 падений. Конфигурация: Попытки использовать Опция Без параллелизма внутри файлов и с небольшим числом потоков время сильно растет — такой режим хоть и стабильный по падениям, но слишком медленный. В итоге мы выявили оптимальную стратегию: запускать Vitest с Настройка Vitest в CIНа наших CI-раннерах ресурсы ограничены: 3 CPU 6 GiB RAM (лимит процесса Node подняли примерно к рамкам ресурсов раннера). Мы повторили замеры производительности в CI. Условия взяли аналогичные локальному измерению, но с учетом ограничений инфраструктуры. Ключевые результаты собрали в таблицу.
Основные наблюдения:
Настройка Vitest практически совпала с рекомендациями официального гайда миграции с Jest — потребовалось лишь минимум изменений и Vitest «из коробки» почти сразу заменил Jest. Проблемы и решения при переходе с Jest на VitestПри переходе на Vitest мы столкнулись с рядом особенностей, которые отсутствовали в Jest. Проблема: Vitest падает с ошибкой Cannot import Vitest outside of test run. Проблема: тесты не изолированы, данные «перетекают» между тестами. Проблема: не все импорты мокируются правильно. Проблема: работа таймеров fake и realTimers в Vitest отличается от Jest. Проблема: шпионы (spy) не срабатывают на внутренние методы. Проблема: неатомарность модулей и скрытые зависимости между ними. Проблема: задержка поддержки NX-плагинов и Allure. Проблема: недостаток документации по Vitest. После решения всех проблем мы выпустили мажорную версию фреймворка и начали миграцию на Vitest во всех клиентских приложениях. Миграция проектов оказалась проще, чем мы ожидали: нам помог инструмент ts2esm, который автоматически перевел кодовую базу с CommonJS на ESM. После этого оставалось только пройтись по коду и вручную поправить не охваченные проблемы, но основную работу проделал именно Результаты миграцииИтоговые метрики не обманули наших ожиданий — скорость полного прогона тестов выросла. В среднем по проектам время регресса сократилось примерно на 6%, а для большинства отдельных сервисов прогон стал быстрее примерно на 25% (кроме монорепозиториев). Не во всех сервисах удалось ускорить прогон. Без Нам помогла практика жесткого контроля окружения. Помимо встроенного механизма retry в Vitest, мы внедрили дополнительный механизм повторных прогонов упавших тестов — мы называем его hardRetry. Если тесты падают, мы собираем список упавших кейсов и сразу запускаем их еще раз в том же контексте, но уже на свежевосстановленном окружении. Мы полностью пересоздаем контейнеры для запуска. Это позволяет стабилизировать результаты CI-прогона, исключая влияние остаточных ошибок, и при этом экономить время и ресурсы раннеров за счет сокращения повторных запусков job. Мы сократили общее время работы джоб с тестами примерно на 21% — с 9583 до 7550 часов в неделю. Уменьшили количество повторных прогонов на ~19% — с 1279 до 1042 запусков в неделю. При этом 95-й перцентиль времени выполнения джобы остался примерно тем же: ~19 минут при ~3800 тестах.
Результаты подтвердили, что переход на Vitest сделал наше тестирование быстрее, стабильнее и эффективнее. Будущее с VitestПереход на Vitest стал отправной точкой для развития нашей тестовой инфраструктуры и повышения эффективности разработки. Vitest дал нам более быструю, стабильную и гибкую платформу, которая позволила настроить процесс тестирования под наши нужды и создать удобные инструменты для команд. Нам видится большой потенциал в развитии системы тестирования на базе Vitest. Это касается не только улучшения инфраструктуры, но и создания инструментов для повышения качества кода и ускорения обратной связи. Мы начали с амбициозной цели — избавиться от нестабильности тестов, ускорить поставку и сделать тестирование неотъемлемой частью процесса разработки. Путь был непростым: от анализа текущего состояния и выбора подходящего инструмента до миграции кода и адаптации инфраструктуры. Планы:
В итоге Vitest стал для нас не просто заменой Jest, а платформой для создания современной, эффективной и расширяемой системы тестирования. Мы уверены, что дальнейшее развитие интеграции Vitest в наш CI/CD позволит повысить качество продукта, ускорить разработку и снизить затраты на тестирование. Используйте удобные инструменты и лучшие практики, опирайтесь на то, что уже работает, но не увязайте в привычном: всегда есть место для улучшения! |