Привет! На связи Дмитрий Чепайкин, старший Angular-разработчик в команде Klecks — платформы для разметки данных AI Центра Т-Банка.

Долгое время unit-тесты фронтенда у нас крутились на Karma + Jasmine. Схема работала, пайплайн зеленый, переписывать «ради моды» не хотелось. Но в какой-то момент Karma официально признали deprecated, а корректная отгрузка отчетов в Allure перестала быть чем-то, на что можно положиться. Расскажу, как мы выбирали замену, почему сначала смотрели на Jest, а в итоге приняли Vitest — и как устроен итоговый сетап.

Триггеры миграции

Во фронтенде Klecks три тестовых проекта: customer, operator и shared. Кодовая база включает сотни *.spec.ts, использует Angular TestBed, моки сервисов, фейковые таймеры и кастомные хелперы вроде UnhandledCatcher. При этом интеграционные сценарии уже давно выполнялись на Playwright, а unit-тестирование продолжало опираться на классический Angular-стек: Karma через builder @angular-devkit/build-angular:karma, Jasmine в качестве тестового API и Chrome Headless для запуска в CI. Покрытие кода собиралось через karma-coverage, а результаты unit-тестов пытались выгружать в Allure вместе с e2e-тестами.

Формально все работало. Но стоимость поддержки стека росла, а на Karma уже не решалась надежно одна из задач качества — нормальная иерархия unit-тестов в Allure. Поэтому стоял вопрос не «Нужен ли нам новый раннер?», а «Какой раннер позволит уйти с deprecated-инструмента и при этом не развалить существующие тесты и отчетность?».

Зачем трогать то, что работает

Karma справлялась со своими задачами годами. Руки до миграции не доходили не потому, что стек был идеален, а потому, что он был «достаточно хорош». Локально ng test открывал браузер, в CI Chrome Headless прогонял suites. Менять инфраструктуру при живом продукте — всегда риск.

Мы собрали список проблем.

Deprecated и тупиковая поддержка. Karma больше не развивается как основной современный test runner, поэтому дальнейшая интеграция с актуальным инструментарием требует все больше поддержки legacy-слоя. Оставаться на Karma — значит тащить устаревающий слой между CLI и тестами, который рано или поздно упрется в несовместимость с новой версией Angular/Webpack/Vite.

Allure для unit-тестов. Для e2e у нас уже был рабочий контур Playwright → Allure. Для unit на Karma отчеты не собирались, так как не было стабильного плагина, полноты и стабильности в выгрузке артефактов тестов. В CI это превращалось в «тесты зеленые, а в Allure unit-слой неполноценный». Именно эта боль стала триггером: отчетность — часть процесса, а не опциональный бантик.

Тяжелый браузерный рантайм в CI. Karma поднимает реальный Chrome. Для части DOM-тестов это плюс, но для сотен unit-спеков — лишняя тяжесть образа, флаки по браузеру и более дорогой пайплайн. Cypress-образ ради unit-прогона сам по себе сигнал, что инфраструктура разъехалась с задачей.

Jasmine-специфика. API привычный, но экосистема вокруг моков, таймеров и матчеров все сильнее расходится с тем, что используют Jest/Vitest-мир и современные Allure-репортеры. Любая новая интеграция требовала прокладок под Karma и Jasmine, а не готового first-class-плагина.

Основными целями миграции стали отказ от устаревшего Karma, обеспечение корректной выгрузки результатов unit-тестов в Allure и сохранение существующей бизнес-логики тестов. Изменения должны были затронуть только инфраструктуру тестирования и API тестового раннера.

Рассматривали: Jest vs Vitest

Мы не прыгали сразу в финальный выбор. Сначала на ветке test_jest собрали полноценный прототип на Jest: jest-preset-angular, отдельные конфиги создали на customer / operator / shared, allure-jest, добавили адаптеры под Jasmine-совместимые хелперы (expectAsync, toBeTrue / toBeFalse, глобальный spyOn). Это был честный spike: можно ли перенести сотни спеков и закрыть Allure.

Эксперимент с Jest показал, что можно закрыть основные боли:

  • нативный Node + jsdom без Chrome;

  • официальный путь через jest-preset-angular;

  • рабочий allure-jest с иерархией лейблов;

  • параллельный прогон проектов.

Но именно для нашего Angular-монорепо появились минусы:

  • отдельный контур мимо Angular builder — скрипты jest --config ... вместо ng test;

  • свой зоопарк конфигов (create-jest-config, tsconfig.jest.json, project configs);

  • необходимость compatibility-слоев под старый Jasmine API;

  • трансформ ESM/CJS-зависимостей (transformIgnorePatterns на длинный список пакетов Taiga/Angular);

  • два мира инструментов: CLI/сборка на одном стеке, тесты — на другом.

Jest как технология нас не провалил. Он показал, что миграция с Karma возможна и что Allure на Node-раннере реально оживает. Но как целевой стек для Angular 21 он выглядел компромиссом, а не финальной точкой.

В итоге мы выбрали Vitest. После Jest-spike мы собрали решение на Vitest — коммит с принятым направлением: миграция builders, конфигов и спеков. Выбор опирался на набор практических критериев.

Интеграция Vitest через Analog позволила сохранить запуск через Angular CLI и стала подходящим вариантом для нашего текущего workspace. Vitest подключается через @analogjs/vitest-angular:test. Для разработчика вход тот же:

ng test customer
ng test operator --configuration=ci

В angular.json вместо karma-builder:

"builder": "@analogjs/vitest-angular:test",
"options": {
"configFile": "vitest.config.mts"
}

Мы остаемся внутри Angular CLI, а не уходим в параллельную систему запуска.

Миграция на Vitest для нас закрыла ряд вопросов.

Vite-based pipeline вместо отдельного Jest transform layer. Vitest работает на Vite: быстрее старт, нативная работа с ESM, проще inline зависимостей Angular/Taiga через server.deps.inline. Там, где в Jest копился transformIgnorePatterns, в Vitest мы описываем resolve/alias и список пакетов для инлайна — и это лучше ложится на текущий фронтенд-стек.

Прямая интеграция с Allure через allure-vitest. allure-vitest/reporter пишет результаты в allure-results/unit/<project>. Плюс общий setup автоматически проставляет иерархию: parentSuite → suite → layer → epic → subEpic.

То есть unit-слой наконец встает рядом с интеграционными отчетами по тем же правилам качества.

Меньше «клея», ближе к целевому API. Vitest из коробки дает vi.fn, vi.spyOn, fake timers, globals. Jasmine → Vitest — механическая замена в спеках, без отдельного compatibility-runtime, который пришлось бы сопровождать на Jest.

CI без браузера и с контролем воркеров. Unit-джоба переехала на Node-образ. Воркеры ограничены явно: в CI VITEST_MAX_WORKERS=2, потому что Angular + jsdom + coverage прожорливы по RAM, а K8s часто отдает чужой CPU count. Это осознанная настройка под нашу инфраструктуру, а не дефолт «съешь все ядра».

Вывод: Jest доказал миграцию, Vitest лучше сел как целевой раннер для Angular-приложений Klecks.

Миграция самих тестов

Большая часть работы — не переизобрести тесты, а перевести API. Вот типичные замены.

Было (Jasmine)

Стало (Vitest)

jasmine.createSpy()

vi.fn()

spyOn(obj, 'method')

vi.spyOn(obj, 'method')

jasmine.createSpyObj(...)

ручной stub / vi.fn на методах

and.returnValue(x)

mockReturnValue(x)

and.callFake(fn)

mockImplementation(fn)

calls.reset()

mockClear() / mockReset()

jasmine.clock().install()

vi.useFakeTimers()

jasmine.clock().tick(n)

vi.advanceTimersByTime(n)

jasmine.objectContaining

expect.objectContaining

done()-колбэки

async/await + Promise

jasmine.Spy в типах

Mock из vitest

TestBed, ComponentFixture, провайдеры и структура describe/it почти не менялись. 

Важно, что миграция раннера не должна была стать рефакторингом продуктового кода. Мы отдельно подтянули инфраструктурные хелперы. Например, UnhandledCatcher научили ловить и window.onerror, и rxjs.config.onUnhandledError. В jsdom/Vitest необработанные ошибки Observable ведут себя иначе, чем в Karma/Chrome. Такие места всплывают только на реальном прогоне, поэтому spike на Jest тоже был полезен: часть проблем API мы увидели еще до финального выбора. 

Allure: как закрыли исходную боль

Что мы сделали по шагам: 

  1. Репортер пишет в allure-results/unit/<project>.

  2. Автолейблы строят иерархию из пути файла и имени теста.

  3. Epic выбирается по расположению: customer / operator / shared.

  4. Локально можно собрать и открыть отчет теми же npm-скриптами, что и для integration

  5. Параллелизм: Chrome Headless / Karma → Vitest threads, maxWorkers=2

В итоге unit-слой перестал быть «второй реальностью» относительно Playwright: и там и там Allure понимает слой, suite и продуктовый epic.

Что принесла миграция на vitest. 

Было

Стало

Раннер

Karma + Jasmine + Chrome

Vitest + jsdom (+ Analog builder)

Статус стека

deprecated / legacy path

Актуальный Angular-friendly-путь

Запуск

ng test через karma-builder

ng test через @analogjs/vitest-angular:test

Allure unit

Ненадежная/неполная выгрузка

allure-vitest + автолейблы

CI-образ

Тяжелый browser-based

Node 22

Параллелизм

Браузерный single-run контур

threads + явный лимит воркеров

Скорость джобы в CI

~3 минуты 40 секунд

~2 минуты 20 секунд

Плюсы, которые мы получили:

  • ушли с deprecated Karma;

  • сохранили привычный DX через ng test;

  • unit-отчеты в Allure стали частью нормального процесса;

  • CI стал проще по зависимостям (без Chrome ради unit);

  • спеки мигрируют механически, без переписывания сценариев;

  • ускорили джобу unit тестов в CI на ~36,4%.

Но есть и компромиссы. Они для нас— разумная цена за поддерживаемый стек и рабочую отчетность. Компромиссы, о которых стоит знать:

  • jsdom ≠ полный браузер: редкие DOM/CSS edge-cases могут вести себя иначе;

  • в CI нельзя бездумно крутить maxWorkers: coverage + Angular TestBed легко упираются в память;

  • часть хелперов (UnhandledCatcher, fake timers) потребовала точечной адаптации под новый рантайм.

Нативный Angular Vitest (@angular/build:unit-test) мы сознательно не брали, потому что для нашего кейса он слишком закрытый. Официальный builder заточен под zero-config: Angular сам поднимает Vitest, а кастомный vitest.config либо ограничен, либо поддерживается частично.

Команда Angular прямо говорила: если нужен полноценный vitest.config.ts (плагины, репортеры, тонкая настройка), берите Analog.

Для нас такой уровень контроля был принципиален. Требовалось интегрировать allure-vitest/reporter с собственной иерархией лейблов, создать единый factory-конфиг для customer, operator и shared с общими алиасами, путями Less и директориями покрытия. Кроме того, необходимо было управлять количеством воркеров через VITEST_MAX_WORKERS, чтобы учитывать ограничения памяти в K8s при запуске Angular-тестов с jsdom и coverage, а также гибко настраивать параметры рантайма, включая isolate, inline deps и setupFiles.

Analog (@analogjs/vitest-angular) дает полный контроль «из коробки» и при этом оставляет ng test. Jest-spike уже показал, что «просто зеленых тестов» недостаточно — нужна управляемая инфраструктура и отчеты.

Native имеет смысл для зеленого поля или простых приложений. Для нашего монорепо с Allure и жестким CI Analog оказался практичнее. Позже, когда @angular/build:unit-test догонит по конфигурируемости, можно будет пересмотреть — но на момент решения Analog закрывал требования лучше.

Заключение

Мы не меняли раннер, потому что «все переходят на Vitest». Триггером стала связка из двух фактов: Karma deprecated и Allure для unit на текущем стеке больше не закрывал требования качества.

Jest на ветке test_jest подтвердил, что Node-раннер + Allure — рабочий путь. Vitest выбрали как целевое решение, потому что он лучше стыкуется с Angular CLI через Analog, естественнее живет с Vite/ESM и дает прямую интеграцию с Allure через allure-vitest без отдельной параллельной инфраструктуры запуска.

Итог миграции:

  • один shared Vitest-конфиг на три проекта;

  • builder в angular.json без Karma;

  • автолейблы Allure в @klecks/testing;

  • механический перевод Jasmine API → Vitest в спеках;

  • CI на Node с ограниченными воркерами;

  • unit-тесты в CI проходят на ~36,4% быстрее;

Если у вас похожий Angular-монолит на Karma и уже болит Allure/поддержка, имеет смысл сначала сделать короткий spike на Jest или Vitest именно с выгрузкой отчетов, а не только с «тесты зеленые локально». У нас зеленость никогда не была главной проблемой. Главным было сделать так, чтобы unit-слой снова был наблюдаемым и поддерживаемым.

Комментарии (0)