Традиційні тестові ранери попереднього покоління (наприклад, 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 | |
| +-------------------------+ | | +-------------------------+ |
+-------------------------------+ +-------------------------------+--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, // Ізоляція V8-контексту між тестовими файлами
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 підтримує вимірювання продуктивності безпосередньо через вбудований API 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), середній час виконання та запас похибки.
Вимкнення ізоляції воркерів (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, код тестів та утиліти тверджень потраплять у клієнтський бандл, невиправдано збільшивши його вагу.
// ❌ Помилка: відсутність директиви 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).
// ❌ Помилка: запуск нативних C++ bindings у worker_threads
export default defineConfig({
test: {
pool: 'threads', // Падіння із segfault при виклику деяких C++ аддонів
},
});
// ✅ Правильне рішення: використання пулу 'forks' для файлів із нативними модулями
export default defineConfig({
test: {
pool: 'forks', // Кожен тест запускається в ізольованому процесі child_process
},
});Визначити кількість доступних віртуальних ядер у середовищі CI (наприклад, 2 vCPU в GitHub Actions або 8 vCPU на виділеному runner).
Використовувати pool: 'threads' для стандартних TS/React проєктів. Перемикатися на pool: 'forks' лише за наявності нативних аддонів C++.
Задати maxThreads рівним (кількість ядер - 1), щоб не блокувати диспетчер операційної системи та I/O цикли.
Увімкнути нативний збір покриття v8 без babel-трансформації та закешувати артефакти між запусками пайплайну.