Vitest
Vitest — фреймворк тестирования, нативно интегрированный с Vite. Быстрый, совместимый с Jest API, поддерживает ESM из коробки.
Зачем нужно
Jest создавался для CommonJS-мира и требует трансформации для ESM/TypeScript. Vitest использует конвейер Vite — тот же конфиг, та же трансформация, мгновенный HMR для тестов. Если проект на Vite, Vitest — естественный выбор.
Где используется
Проекты на Vite (Vue, React, Svelte), библиотеки с ESM, новые проекты без legacy CJS-зависимостей.
Предпосылки
Настройка Jest, Основы тестирования, знакомство с Vite
Установка
npm install -D vitest
Конфигурация в vite.config.ts
// vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite';
export default defineConfig({
test: {
globals: true, // describe, test, expect без импортов
environment: 'jsdom', // 'node' | 'jsdom' | 'happy-dom'
include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],
coverage: {
provider: 'v8', // 'v8' или 'istanbul'
reporter: ['text', 'lcov'],
include: ['src/**/*.{js,ts}'],
exclude: ['src/**/*.test.{js,ts}'],
},
setupFiles: ['./src/test/setup.ts'],
},
});
Или отдельный vitest.config.ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
});
Скрипты в package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:watch": "vitest --watch",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Совместимость с Jest API
Vitest повторяет API Jest почти полностью:
// Работает и в Jest, и в Vitest
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
describe('Calculator', () => {
test('складывает числа', () => {
expect(1 + 2).toBe(3);
});
test('объекты', () => {
expect({ a: 1 }).toEqual({ a: 1 });
});
test('массивы', () => {
expect([1, 2, 3]).toContain(2);
});
test('исключения', () => {
expect(() => { throw new Error('oops'); }).toThrow('oops');
});
});
vi вместо jest
// Jest:
const fn = jest.fn;
jest.mock('./module');
jest.spyOn(obj, 'method');
jest.useFakeTimers;
// Vitest:
const fn = vi.fn;
vi.mock('./module');
vi.spyOn(obj, 'method');
vi.useFakeTimers;
Главные отличия от Jest
1. ESM по умолчанию
// Vitest: import/export работает из коробки
import { formatPrice } from './utils';
test('форматирует цену', () => {
expect(formatPrice(1999)).toBe('$19.99');
});
2. Watch mode по умолчанию
vitest # watch mode (как jest --watch)
vitest run # одноразовый запуск (как jest)
3. Скорость (Vite transform)
Проект 200 тестов:
Jest: ~8 секунд
Vitest: ~2 секунды (первый запуск), ~0.5с (HMR)
4. Встроенный UI
npm install -D @vitest/ui
vitest --ui
# Открывается localhost:51204 с визуальным интерфейсом
5. In-source testing (тесты внутри исходника)
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// Тест прямо в файле! Удаляется при сборке.
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
test('add', () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
});
}
// vite.config.ts — включение in-source testing
export default defineConfig({
define: {
'import.meta.vitest': 'undefined', // удаляется в production
},
test: {
includeSource: ['src/**/*.ts'],
},
});
Моки в Vitest
vi.fn
const mockFn = vi.fn;
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'ok' });
mockFn.mockImplementation((x) => x * 2);
vi.mock
vi.mock('./api', () => ({
fetchUsers: vi.fn.mockResolvedValue(),
fetchPosts: vi.fn.mockResolvedValue(),
}));
// Частичный мок
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
formatDate: vi.fn.mockReturnValue('01.01.2025'),
};
});
vi.spyOn
const spy = vi.spyOn(console, 'log');
doSomething;
expect(spy).toHaveBeenCalledWith('hello');
spy.mockRestore();
Fake timers
test('debounce', () => {
vi.useFakeTimers;
const fn = vi.fn;
const debounced = debounce(fn, 300);
debounced;
debounced;
debounced;
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce;
vi.useRealTimers;
});
Snapshot-тесты
test('snapshot', () => {
const user = createUser('Алиса');
expect(user).toMatchSnapshot;
});
// Inline snapshot
test('inline snapshot', () => {
expect(formatDate(new Date('2025-01-01'))).toMatchInlineSnapshot('"01.01.2025"');
});
Type-safe тестирование
import { expectTypeOf } from 'vitest';
test('типы', () => {
expectTypeOf(add).toBeFunction;
expectTypeOf(add(1, 2)).toBeNumber;
expectTypeOf<string>.toMatchTypeOf<string | number>;
expectTypeOf({ name: 'test' }).toMatchTypeOf<{ name: string }>;
});
Миграция с Jest
| Jest | Vitest | Изменение |
|---|---|---|
jest.fn |
vi.fn |
Замена префикса |
jest.mock |
vi.mock |
Замена префикса |
jest.spyOn |
vi.spyOn |
Замена префикса |
jest.useFakeTimers |
vi.useFakeTimers |
Замена префикса |
jest.config.js |
vite.config.ts → test |
Другой формат конфига |
@jest/globals |
vitest |
Другой пакет для импортов |
Автоматическая миграция
npx vitest --reporter=verbose --run
# Если globals: true — тесты работают без изменений
# Замени jest.* на vi.* — готово
Частые ошибки
1. globals: false и забытые импорты
// Если globals: false, нужен явный импорт
import { describe, test, expect, vi } from 'vitest';
2. vi.mock с ESM
// ПЛОХО: factory должна быть синхронной для top-level
vi.mock('./api', async () => { // async может вызвать проблемы
return { fetch: vi.fn };
});
// ХОРОШО: используй vi.importActual внутри
vi.mock('./api', async () => {
const actual = await vi.importActual<typeof import('./api')>('./api');
return { ...actual, fetch: vi.fn };
});
3. Не подключили environment для DOM
// Тест использует document, но environment: 'node'
// Ошибка: document is not defined
// Решение: environment: 'jsdom' или 'happy-dom'
Практика
- Настрой Vitest в существующем Vite-проекте (или создай новый
npm create vite) - Напиши тесты с
vi.fnиvi.mockдля API-сервиса - Попробуй in-source testing: добавь тест внутри файла утилит
- Используй
vitest --uiдля визуального просмотра тестов - Мигрируй один Jest-тестовый файл на Vitest (замени jest.* на vi.*)