Vitest vs Jest: миграция большого проекта
Я недавно мигрировала проект с Jest на Vitest. Большой проект: 4 800 тестов, кастомные раннеры, моки модулей, snapshots, coverage с отчётом для CI. На старте задачи я ожидала ад, но получилось аккуратно — за неделю с парой откатов. Расскажу детали, чтобы у вас прошло плавнее, чем у меня.
Почему мигрировать
На том проекте Jest держал нас за горло. 4 800 тестов запускались 4 минуты, и медленнее, чем Vitest на той же машине, в 4 раза. На больших фичах ребята перестали запускать локально — гнали в CI, ждали 6-8 минут.
Vitest даёт три выигрыша:
- Скорость. На прогон ушло меньше минуты.
- Совместимость с Vite. Один сборщик, один конфиг, один resolve.
- Современный API. Тестовые fn-сигнатуры умеют типы лучше Jest.
Если у тебя проект на webpack или CRA — Vitest ставится сложнее. Если на Vite, Astro, Next.js или любом современном сборщике — это естественный шаг.
Базовая совместимость
Vitest проектировался как drop-in replacement Jest. Большая часть API идентична: describe, it, expect, beforeEach, afterAll, vi.fn() вместо jest.fn().
// до — на Jest
import { fetchUser } from './api';
describe('fetchUser', () => {
it('returns user', async () => {
const user = await fetchUser('1');
expect(user.id).toBe('1');
});
});На Vitest этот код запустится без изменений. Но есть тонкости — о них ниже.
Конфигурация
В отличие от Jest, у Vitest конфиг идёт через Vite — обычно прямо в vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
include: ['src/**/*.{test,spec}.{ts,tsx}'],
},
});Поле globals: true делает describe, it, expect доступными без импорта (как в Jest). Без этого нужно импортировать их явно из vitest.
Если ты на Astro или Vite-проекте — конфигурация уже готова, добавляешь test-секцию в существующий vite.config.ts:
/// <reference types="vitest" />
import { defineConfig } from 'vite';
export default defineConfig({
// ... обычная Vite-конфигурация
test: {
// ...
},
});Замена jest на vi
Самая массовая правка. В Vitest все Jest-методы доступны через vi:
// Jest
jest.fn();
jest.mock('./api');
jest.spyOn(obj, 'method');
jest.useFakeTimers();
// Vitest
import { vi } from 'vitest';
vi.fn();
vi.mock('./api');
vi.spyOn(obj, 'method');
vi.useFakeTimers();Я использовала regex замену: jest\. → vi.. На 4 800 тестов закрыло 90% мест. Оставшиеся 10% — кастомные хелперы, которые я переписывала руками.
В Vitest есть compat-режим: можно настроить алиас jest = vi, и старые тесты будут работать без замены. Я этим не воспользовалась — лучше один раз пройтись по кодовой базе, чем держать виртуальный Jest навсегда.
Моки модулей
Самое тонкое место. Jest использует CommonJS-модель загрузки и встраивает моки на уровне runtime. Vitest — ESM-первый, и моки работают через перехват импортов.
// Jest — работает
jest.mock('./api', () => ({
fetchUser: jest.fn(),
}));
// Vitest — синтаксис тот же
vi.mock('./api', () => ({
fetchUser: vi.fn(),
}));Здесь несовместимость минимальная. Но есть случаи, где Vitest строже:
Hoisted моки
В Jest jest.mock поднимался компилятором в начало файла. В Vitest — тот же механизм, но через vi.hoisted для значений, которые нужны при объявлении мока:
// Vitest
const { mockedFn } = vi.hoisted(() => ({ mockedFn: vi.fn() }));
vi.mock('./api', () => ({ fetchUser: mockedFn }));Без vi.hoisted функция mockedFn на этапе мока ещё не существует, и тест падает.
Mock внутри describe
В Vitest vi.mock на уровне модуля поднимается выше всех импортов. Если ты вызываешь vi.mock внутри beforeEach или describe — мок применится непредсказуемо. На моём проекте было четыре таких места, я их переписала на верхний уровень + vi.mocked() для типизации.
Snapshot
Snapshot тесты переезжают как есть. Vitest читает __snapshots__/-папки и обновляет их по тому же протоколу. На моём проекте 350 снапшотов перешли без правок.
Один нюанс: формат serializer'ов. Если у тебя были кастомные сериализаторы для Jest (например, для дат или Decimal-чисел), в Vitest подключаются через setup:
// tests/setup.ts
import { expect } from 'vitest';
import { dateSerializer } from './serializers';
expect.addSnapshotSerializer(dateSerializer);Coverage
Vitest по умолчанию использует v8-coverage. Это быстрее, чем istanbul, но reports немного отличаются. На том проекте я переключилась на istanbul-провайдер — у него больше поддержки в нашем CI и в SonarQube:
test: {
coverage: {
provider: 'istanbul',
reporter: ['text', 'lcov', 'html'],
exclude: ['**/*.config.ts', '**/types.ts'],
},
},Если CI просто читает lcov-файл, оба провайдера работают. Если использует более сложные инструменты — istanbul безопаснее.
Watch и UI
Vitest watch ощутимо приятнее Jest watch. Он использует Vite HMR и пересобирает только изменённое. У меня типичный сценарий: изменяешь файл, ждёшь 100-200 мс, тесты прогоняются. На Jest было 1-2 секунды.
Vitest UI — отдельный режим:
pnpm vitest --uiОткрывается веб-интерфейс с деревом тестов, фильтрами, коду. Это что-то среднее между Jest UI и встроенным интерфейсом VS Code. Если редко используешь — можно не ставить.
Параллельность
В Vitest каждый файл тестов запускается в отдельном worker. Контекст изолированный, моки не утекают между файлами. Это иногда строже Jest — если ваш тест зависел от global state другого теста, он упадёт.
На моём проекте мы поймали 8 таких мест: моки localStorage, общий jest.useFakeTimers() между файлами. Все эти случаи — багги, и Vitest их подсветил. Хороший повод почистить.
Скорость на проекте
- Jest, parallel: ~245 секунд.
- Jest, parallel + SWC transformer: ~140 секунд.
- Vitest: ~52 секунды.
- Vitest + Bun runner: ~38 секунд.
Vitest на CI дал нам 4-5x ускорения по чистому времени тестов. С учётом времени установки и подготовки — общее время прогона CI упало с 8 до 3 минут.
Что я бы делала иначе
Я начала миграцию с попытки переписать всё за один PR. Получился monstrous diff из 200+ файлов, которые никто не мог нормально проревьюить. Через два дня откатилась.
Сделала по-другому: добавила Vitest в проект параллельно с Jest, мигрировала пакетами по 200-300 тестов, мерджила маленькими PR. Каждый PR ревьюился за час, и команда видела прогресс. Финальный шаг — удаление Jest и его конфигов.
Что копать дальше
Vitest — это не «Jest, только быстрее». Это переосмысленный API с собственными концепциями: workspaces, browser-mode, in-source testing. После миграции стоит посмотреть на эти возможности — они закрывают сценарии, которые в Jest требовали танцев. Особенно in-source testing мне понравился: мелкие функции тестируются прямо в файле кода без отдельных .test.ts.