lenec ru

← все посты

Vitest vs Jest: миграция большого проекта

17K

Я недавно мигрировала проект с 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.

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

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.