На протяжении долгого времени де-факто стандартом тестирования в экосистеме JavaScript оставался Jest. Однако фундаментальная архитектура Jest сформировалась в эпоху CommonJS и синхронных загрузчиков модулей require(). В современных веб-приложениях с нативными ECMAScript Modules (ESM), компиляторами TypeScript, JSX и бандлерами нового поколения Jest требует громоздкой надстройки: тяжелых плагинов трансформации (ts-jest, @swc/jest, babel-jest), сложной настройки регулярных выражений transformIgnorePatterns и ручного мокирования асинхронного импорта ESM-пакетов.
Vitest спроектирован с чистого листа для устранения этого разрыва. Vitest не изобретает собственный компилятор, а использует единый пайплайн трансформации Vite. Это означает, что конфигурация алиасов путей (@/*), обработка CSS-модулей, JSX-синтаксиса React 19 и переменных окружения работают в тестах абсолютно идентично тому, как они работают в основном приложении.
Ключевые архитектурные преимущества Vitest:
worker_threads Node.js, обеспечивая полную изоляцию контекстов памяти и линейное ускорение на многоядерных процессорах.Взаимодействие компонентов в процессе запуска тестов Vitest можно представить как конвейер, связывающий файловую систему, граф зависимостей и изолированные потоки выполнения:
[Файловая система: *.ts / *.tsx]
│
▼
[Vite Transformation Pipeline] <- Обработка TypeScript, JSX, alias '@/*'
│
▼
[Модульный граф Vite] <- Определение затронутых тестов в Watch-режиме
│
▼
[Tinypool Worker Pool] <- Параллельное распределение задач
┌───────────┬───────────┐
▼ ▼ ▼
[Worker 1] [Worker 2] [Worker 3] <- Изолированные контексты рантайма:
(node) (happy-dom) (happy-dom) 'node' (быстрый) или 'happy-dom' (DOM API)
│ │ │
└───────────┼───────────┘
▼
[Консольный репортер] <- Мгновенный вывод Pass/Fail и интерактивный CLIТестовый раннер изолирует каждый тестовый файл в отдельном контексте памяти. Глобальные переменные или состояние, мутированное в user.test.ts, гарантированно не попадут в cart.test.ts.
Добавление пакетов vitest и опционального эмулятора DOM (happy-dom) в секцию devDependencies через пакетный менеджер pnpm.
Определение окружения исполнения (node или happy-dom), алиасов путей проекта и шаблонов поиска тестовых файлов.
Создание модуля с расширением .test.ts или .spec.ts, импорт функций describe, it, expect и определение тест-кейсов.
Использование pnpm vitest для локальной быстрой разработки с перезапуском по изменению кода или pnpm vitest run для разового прогона в CI.
За счет чего Vitest обеспечивает кратно более высокую скорость запуска и отсутствие проблем с компиляцией TypeScript по сравнению с Jest?
pnpm vitest: время обратной связи между сохранением файла в VS Code и получением вердикта теста составляет менее 50 миллисекунд.vitest.workspace.ts для запуска тестов в десятках пакетов монорепозитория с разделением бэкенд-пакетов (environment: 'node') и фронтенд-библиотек (environment: 'happy-dom').Установим Vitest и окружение happy-dom в проект с помощью pnpm:
pnpm add -D vitest happy-dom @types/nodeСоздадим конфигурационный файл vitest.config.ts в корне проекта:
import { defineConfig } from 'vitest/config';
import path from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
// Включение глобальных переменных (describe, it, expect) без явного импорта
// Для строгой типизации рекомендуется оставить globals: false
globals: false,
// Окружение по умолчанию: 'node' (для чистого TS) или 'happy-dom' (для DOM)
environment: 'node',
// Маски поиска тестовых файлов
include: ['src/**/*.{test,spec}.{ts,tsx}'],
// Исключаемые директории
exclude: ['node_modules', 'dist', '.next'],
// Конфигурация генератора покрытия кода
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/**/*.d.ts',
'src/**/index.ts',
],
},
},
});Выбор окружения выполнения (environment) напрямую влияет на скорость выполнения тестов и потребление оперативной памяти.
[!TIP] Вы можете переопределять окружение точечно для каждого файла с помощью директивы в самом верху файла:
typescript// @vitest-environment happy-domЭто позволяет держать глобальным быстрое окружение
node, аhappy-domподключать только там, где реально тестируются React-компоненты.
Создадим простой служебный модуль форматирования чисел и денежных значений:
export interface FormatCurrencyOptions {
readonly locale?: string;
readonly currency?: string;
}
export function formatCurrency(
amountInCents: number,
options: FormatCurrencyOptions = {}
): string {
if (!Number.isFinite(amountInCents)) {
throw new TypeError('Сумма должна быть конечным числом');
}
const { locale = 'ru-RU', currency = 'RUB' } = options;
const majorUnits = amountInCents / 100;
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(majorUnits);
}Теперь создадим файл спецификации src/utils/formatCurrency.test.ts:
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './formatCurrency';
describe('formatCurrency', () => {
it('корректно конвертирует копейки в рубли с правильным символом валюты', () => {
// 150 000 копеек = 1 500,00 ₽
const formatted = formatCurrency(150000, { locale: 'ru-RU', currency: 'RUB' });
// Заменяем неразрывные пробелы \u00A0 на обычные для надежного сравнения
const normalized = formatted.replace(/\u00A0/g, ' ');
expect(normalized).toBe('1 500,00 ₽');
});
it('форматирует доллары США с дефолтной локалью', () => {
const formatted = formatCurrency(4999, { locale: 'en-US', currency: 'USD' });
expect(formatted).toBe('$49.99');
});
it('выбрасывает TypeError при передаче NaN или Infinity', () => {
expect(() => formatCurrency(Number.NaN)).toThrow(TypeError);
expect(() => formatCurrency(Number.POSITIVE_INFINITY)).toThrow(
'Сумма должна быть конечным числом'
);
});
});Отслеживание событий сохранения файлов в исходном коде и тестах с дебаунсом 100 мс.
Исключает лишние повторные запуски при пакетных сохранениях IDE.
Поиск в графе зависимостей Vite всех файлов тестов, которые прямо или косвенно импортируют измененный модуль.
Если изменен файл `formatCurrency.ts`, перезапустится только `formatCurrency.test.ts`, но не весь проект.
Передача задачи на исполнение в пул изолированных воркеров worker_threads.
Каждый воркер имеет чистую песочницу памяти для предотвращения утечек состояния.
Вывод детального статуса выполнения, времени работы и предоставление команд фильтрации.
Горячие клавиши позволяют фильтровать запуск по регулярным выражениям прямо в терминале.
Запуск тестов в консоли:
# Интерактивный watch-режим для локальной разработки
pnpm vitest
# Разовый прогон для CI/CD с кодом завершения процесса
pnpm vitest run
# Прогон с генерацией таблицы покрытия кода
pnpm vitest run --coverageВ интерактивном Watch-режиме доступны горячие клавиши:
p — фильтрация по имени файла (File Name Pattern): введите часть пути или имени файла, чтобы запускать только его;t — фильтрация по названию теста (Test Name Pattern): запускает только тесты, чье имя внутри it(...) совпадает с шаблоном;f — запуск только упавших (Failed) тестов;u — обновление снапшотов (Update Snapshots);q — выход из Watch-режима.Почему для модульного тестирования чистых утилит и валидаторов предпочтительно указывать environment: 'node', а не 'happy-dom' или 'jsdom'?
Если запустить команду pnpm vitest без флага run в пайплайне GitHub Actions или GitLab CI, процесс перейдет в интерактивный Watch-режим ожидания ввода с клавиатуры. Пайплайн зависнет до истечения таймаута раннера (обычно 60 минут).
// ❌ package.json: зависание в CI
{
"scripts": {
"test:ci": "vitest"
}
}
// ✅ package.json: корректный однократный прогон
{
"scripts": {
"test": "vitest",
"test:ci": "vitest run"
}
}jsdom эмулирует огромный спектр спецификаций браузера, что приводит к медленному старту тестов и высокому потреблению оперативной памяти (до 200–500 МБ на поток).
// ❌ Медленно: jsdom глобально для всех 500 тестов бэкенда и утилит
export default defineConfig({
test: {
environment: 'jsdom',
},
});
// ✅ Быстро: глобальный 'node' и точечный 'happy-dom' через директиву в UI-файлах
// В файле Button.test.tsx:
// @vitest-environment happy-domЕсли в tsconfig.json объявлен путь @/*, но он забыт в секции resolve.alias файла vitest.config.ts, TypeScript в редакторе не покажет ошибок, однако при запуске Vitest выдаст ошибку Error: Failed to resolve import "@/utils/formatCurrency".
// ✅ Обязательно синхронизируйте alias в vitest.config.ts
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
}Метод it.only(...) (или fit(...)) изолирует запуск одного теста во время локальной отладки. Если отправить такой коммит в репозиторий, CI выполнит только этот один тест и отметит билд зеленым, полностью пропустив остальные 99% проверок приложения.
// ❌ Опасно: пропустит все остальные тесты в CI
it.only('проверяет форматирование валюты', () => { ... });
// ✅ Безопасно: используйте флаг --no-focused-tests в CI или линтер eslint-plugin-vitest
// Vitest в CI можно запускать с флагом: vitest run --allowOnly=false