Традиционные тестовые раннеры предыдущего поколения (например, Jest на базе Babel/Webpack) тратили значительную часть времени на предварительную компиляцию, сборку бандлов и трансформацию CommonJS/ESM. В архитектуре Vitest тестовый раннер встроен непосредственно в сервер разработки Vite.
Архитектура Vitest опирается на три ключевых компонента:
vite.config.ts), плагины (Rollup/Vite) и алиасы путей.import / require, циклические зависимости и проблемы с import.meta.+-------------------------------------------------------------------------+
| Vitest Main Process |
| +---------------------------+ +-------------------------------+ |
| | Vite Dev Server (HMR) | <---> | Config & Module Graph Resolver| |
| +---------------------------+ +-------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------------+ |
| | Pool Manager (tinypool / forks) | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
|
+-----------------------+-----------------------+
| |
v v
+-------------------------------+ +-------------------------------+
| Worker 1 (worker_threads) | | Worker N (worker_threads) |
| +-------------------------+ | | +-------------------------+ |
| | V8 Context / Environment| | | | V8 Context / Environment| |
| | (happy-dom / jsdom/node)| | | | (happy-dom / jsdom/node)| |
| +-------------------------+ | | +-------------------------+ |
| | Test Suite: auth.test.ts| | | | Test Suite: user.test.ts| |
| +-------------------------+ | | +-------------------------+ |
| | Vitest Runtime Runner | | | | Vitest Runtime Runner | |
| +-------------------------+ | | +-------------------------+ |
+-------------------------------+ +-------------------------------+Определить число доступных виртуальных ядер в среде CI (например, 2 vCPU в GitHub Actions или 8 vCPU в выделенном runner).
Использовать pool: 'threads' для стандартных TS/React проектов. Переключиться на pool: 'forks' только при наличии нативных аддонов C++.
Задать maxThreads равным (число ядер - 1), чтобы не блокировать планировщик операционной системы и системные прерывания.
Включить нативный сбор покрытия v8 без babel-трансформации и закэшировать артефакты между прогонами пайплайна.
В каком сценарии инженер ОБЯЗАН переключить пул исполнения Vitest с дефолтного pool: 'threads' на pool: 'forks'?
--watch): Использование единого графа модулей Vite позволяет Vitest инвалидировать и перезапускать только те тесты, которые напрямую или транзитивно импортируют измененный файл.bench()) в том же окружении и синтаксисе, что и тесты..snap файлов.vitest.config.ts#Конфигурация Vitest 4+ позволяет детально управлять пулами исполнения, таймаутами, изоляцией и тестовыми средами.
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
define: {
// Вырезание in-source тестов при сборке
'import.meta.vitest': 'undefined',
},
test: {
// Включение in-source тестирования
includeSource: ['src/**/*.{ts,tsx}'],
// Выбор пула: 'threads' | 'forks' | 'vmThreads'
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 8,
minThreads: 2,
isolate: true, // Изоляция контекста между файлами тестов
useAtomics: true, // Использование Atomics и SharedArrayBuffer для синхронизации
},
},
// Окружение по умолчанию для DOM-тестов
environment: 'happy-dom',
// Тайм-аут выполнения одного теста (мс)
testTimeout: 5000,
// Сбор покрытия через движок V8 (нативный профайлер без babel-плагинов)
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
// Запуск бенчмарков
benchmark: {
include: ['src/**/*.bench.{ts,tsx}'],
reporters: ['default'],
},
},
});Концепция in-source testing позволяет хранить модульные тесты для приватных функций и математических утилит непосредственно внутри исходного файла. Это исключает раздувание файловой структуры проекта микро-тестами.
При компиляции приложения в продакшн блок if (import.meta.vitest) полностью удаляется с помощью механизма DCE (Dead Code Elimination).
/**
* Валидация номеров банковских карт и идентификаторов по алгоритму Луна.
*/
export function isValidLuhn(cardNumber: string): boolean {
const sanitized = cardNumber.replace(/\D/g, '');
if (sanitized.length < 13 || sanitized.length > 19) {
return false;
}
let sum = 0;
let shouldDouble = false;
for (let i = sanitized.length - 1; i >= 0; i--) {
const char = sanitized.charAt(i);
let digit = parseInt(char, 10);
if (Number.isNaN(digit)) {
return false;
}
if (shouldDouble) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
// In-source тесты: исполняются раннером Vitest, но вырезаются из продакшн-билда
if (import.meta.vitest) {
const { describe, it, expect } = import.meta.vitest;
describe('isValidLuhn (In-source Test Suite)', () => {
it('подтверждает корректность тестового номера карты', () => {
// Валидный номер по стандарту Луна
expect(isValidLuhn('49927398716')).toBe(true);
});
it('отклоняет номера с некорректной контрольной суммой', () => {
expect(isValidLuhn('49927398717')).toBe(false);
});
it('отклоняет строки с недопустимой длиной', () => {
expect(isValidLuhn('12345')).toBe(false);
expect(isValidLuhn('')).toBe(false);
});
it('корректно игнорирует пробелы и дефисы форматирования', () => {
expect(isValidLuhn('4992-7398-716')).toBe(true);
expect(isValidLuhn('4992 7398 716')).toBe(true);
});
});
}Для корректной работы TypeScript с import.meta.vitest добавьте типы в конфигурационный файл типов:
/// <reference types="vite/client" />
/// <reference types="vitest/importMeta" />Файловые снапшоты (.snap) часто приводят к проблеме «усталости от снапшотов» (Snapshot Fatigue): разработчики слепо обновляют их флагом -u, не читая изменения на сотни строк. Inline Snapshot сохраняет ожидаемый результат прямо в аргументе метода toMatchInlineSnapshot(), делая тест самодокументируемым.
import { describe, it, expect } from 'vitest';
interface AuditLog {
eventId: string;
timestamp: number;
actor: {
id: string;
role: 'admin' | 'editor';
};
payload: Record<string, unknown>;
}
function createAuditRecord(actorId: string, role: 'admin' | 'editor', action: string): AuditLog {
return {
eventId: `evt_${crypto.randomUUID()}`,
timestamp: Date.now(),
actor: { id: actorId, role },
payload: { action, ip: '127.0.0.1' },
};
}
describe('createAuditRecord', () => {
it('формирует детерминированную структуру аудита с динамическими полями', () => {
const record = createAuditRecord('usr_42', 'admin', 'UPDATE_ROLE');
// Использование Property Matchers для нетерминированных значений (UUID, Timestamp)
expect(record).toMatchInlineSnapshot(
{
eventId: expect.stringMatching(/^evt_[0-9a-f-]+$/),
timestamp: expect.any(Number),
},
`
{
"actor": {
"id": "usr_42",
"role": "admin",
},
"eventId": StringMatching /^evt_[0-9a-f-]+$/,
"payload": {
"action": "UPDATE_ROLE",
"ip": "127.0.0.1",
},
"timestamp": Any<Number>,
}
`
);
});
});Vitest включает первоклассную поддержку производительностных тестов через встроенный модуль bench(). Это позволяет сравнивать алгоритмические решения без установки сторонних утилит.
import { bench, describe } from 'vitest';
describe('Поиск элемента: Array.includes vs Set.has', () => {
const sampleSize = 10_000;
const targetId = 'item-9999';
const rawArray: string[] = Array.from({ length: sampleSize }, (_, i) => `item-${i}`);
const lookupSet: Set<string> = new Set(rawArray);
bench('Array.includes (Линейная сложность O(N))', () => {
rawArray.includes(targetId);
});
bench('Set.has (Константная амортизированная сложность O(1))', () => {
lookupSet.has(targetId);
});
});Запуск командой npx vitest bench выводит подробную таблицу: операции в секунду (ops/sec), среднее время выполнения и запас погрешности.
Какая конфигурация сборщика Vite обязательна, чтобы in-source тесты (import.meta.vitest) полностью исключались из клиентского бандла при сборке проекта?
Отключение изоляции воркеров (isolate: false) ускоряет выполнение тестов, но приводит к тому, что мутации window, document, глобальных моков или синглтонов перетекают из одного тестового файла в другой.
// ❌ Антипаттерн: глобальная мутация без последующей очистки
it('мутирует глобальный заголовок авторизации', () => {
globalThis.authToken = 'test-token';
// Если isolate: false, этот токен останется для всех последующих файлов!
});
// ✅ Правильное решение: оставлять isolate: true для сьютов с сайд-эффектами
// и всегда гарантировать сброс в afterEach
afterEach(() => {
delete (globalThis as Record<string, unknown>).authToken;
});Если в конфигурации сборщика не задать флаг удаления кода import.meta.vitest, код тестов и библиотека assertions попадут в продакшн-бандл клиентов, увеличив вес бандла на десятки килобайт.
// ❌ Ошибка: отсутствие директивы define в vite.config.ts
export default defineConfig({
// Без 'define' блок `if (import.meta.vitest)` не вырежется при tree-shaking
});
// ✅ Правильное решение: явная замена идентификатора на undefined
export default defineConfig({
define: {
'import.meta.vitest': 'undefined',
},
});Сохранение гигантского HTML-вывода компонента (более 200 строк) в .snap файл лишает снапшот смысла: при малейшем изменении верстки разработчики автоматически нажимают vitest -u, не вчитываясь в диффы.
// ❌ Антипаттерн: снапшот всего DOM-дерева страницы со всеми дочерними элементами
expect(container).toMatchSnapshot();
// ✅ Правильное решение: проверка точечных семантических инвариантов
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Панель управления');
expect(screen.getAllByRole('listitem')).toHaveLength(5);Некоторые нативные Node.js аддоны (например, старые версии canvas или модули шифрования) не поддерживают мультипоточность внутри одного процесса и вызывают аварийное завершение процесса (Segmentation fault).
// ❌ Ошибка: запуск native C++ bindings в worker_threads
export default defineConfig({
test: {
pool: 'threads', // Падение с segfault при вызове некоторых C++ аддонов
},
});
// ✅ Правильное решение: использование пула 'forks' для файлов с нативными модулями
export default defineConfig({
test: {
pool: 'forks', // Каждый тест запускается в изолированном процессе child_process
},
});