Vitest — тестовый раннер, который использует тот же конвейер преобразований, что и Vite. Он понимает ESM и TypeScript без дополнительной настройки, запускает тесты в пуле воркеров и умеет работать в режиме наблюдения, перезапуская только затронутые файлы. Для проекта на Vite это самый короткий путь от «тестов нет» до покрытия ключевых модулей. Ниже — установка, конфигурация, мокинг, покрытие и ошибки, которые встречаются чаще всего.
Зачем Vitest рядом с Vite
Старые проекты часто несут Jest с отдельным транспайлером, маппингом модулей и набором плагинов. Vitest переиспользует конфигурацию Vite: алиасы, плагины и обработку CSS он берёт из того же места, поэтому тестовое окружение перестаёт отличаться от сборки. Это снижает класс ошибок «в тестах работает, в браузере нет».
API при этом знаком: describe, it, expect, beforeEach и мок-функции выглядят почти как в Jest. Переезд обычно сводится к замене импортов и настройке окружения. Ограничение тоже есть: Vitest рассчитан на проекты, где уже есть Vite или где допустимо его добавить.
Установка и первый тест
Проще всего начать с шаблона Vite, а затем добавить раннер и покрытие. Пакет jsdom нужен только для тестов, которые работают с DOM; для серверного кода достаточно окружения node.
npm create vite@latest vitest-demo -- --template react-ts
cd vitest-demo
npm install
npm install -D vitest @vitest/coverage-v8 jsdom
Затем добавьте скрипты. Режим наблюдения удобен локально, а vitest run обязателен в CI: он завершается с ненулевым кодом при падении и не ждёт изменений файлов.
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
Первый тест кладите рядом с модулем: src/lib/price.ts и src/lib/price.test.ts. Такое соседство упрощает навигацию и позволяет команде vitest related точно сопоставлять изменённый файл с его тестами.
Конфигурация
Отдельный vitest.config.ts подходит, когда тестовое окружение отличается от сборки. Если в проекте уже есть vite.config.ts, добавьте секцию test прямо в него и подключите типы через /// <reference types="vitest/config" />, иначе TypeScript не увидит новое поле. Альтернатива — собрать конфиг функцией mergeConfig из пакета vite.
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
restoreMocks: true,
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: { lines: 80, functions: 80, branches: 70, statements: 80 },
exclude: ['src/main.tsx', '**/*.d.ts'],
},
},
})
Флаг globals избавляет от импорта функций в каждом файле, но тогда в tsconfig.json нужно указать "types": ["vitest/globals"]. Без этого редактор будет подчёркивать describe как неизвестное имя, хотя тесты запустятся. Файл setupFiles подключается до каждого тестового модуля: там место для полифилов, расширений expect и глобальной очистки. Создайте его, даже если он пока пуст.
Тесты, параметризация и мокинг
Проверяйте поведение, а не реализацию. Если тест ломается от переименования внутренней переменной, он написан неправильно. Для набора однотипных случаев используйте it.each — так таблица значений остаётся рядом с проверкой.
import { describe, it, expect } from 'vitest'
import { formatPrice } from './price'
describe('formatPrice', () => {
it('добавляет разделитель разрядов', () => {
expect(formatPrice(1500)).toBe('1 500 ₽')
})
it.each([[0, '0 ₽'], [99, '99 ₽'], [1000, '1 000 ₽']])(
'%i превращается в %s',
(input, expected) => {
expect(formatPrice(input)).toBe(expected)
},
)
})
Внешние зависимости заменяются через vi.fn и vi.stubGlobal. Важно возвращать окружение в исходное состояние: restoreMocks в конфиге снимает шпионов, но глобальные подмены нужно убирать явно в afterEach.
import { it, expect, vi, afterEach } from 'vitest'
import { loadUser } from './api'
afterEach(() => vi.unstubAllGlobals())
it('разбирает ответ API', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 7, name: 'Анна' }),
})
vi.stubGlobal('fetch', fetchMock)
await expect(loadUser(7)).resolves.toEqual({ id: 7, name: 'Анна' })
expect(fetchMock).toHaveBeenCalledWith('/api/users/7')
})
Таймеры тестируются виртуальными часами: vi.useFakeTimers() перехватывает setTimeout и setInterval, а advanceTimersByTime прокручивает время без реального ожидания. Не забудьте vi.useRealTimers() в конце, иначе следующий тест может зависнуть на ожидании промиса.
Покрытие и CI
Порог покрытия полезен как защита от регрессии, но только вместе с осмысленными тестами. Сто процентов покрытия по строкам легко получить, вызывая код без единой проверки. Начните с 70–80 процентов по ключевым модулям и повышайте планку постепенно.
npx vitest run --coverage
npx vitest run --reporter=default --reporter=junit --outputFile=reports/junit.xml
npx vitest related src/lib/price.ts
npx vitest run --coverage --silent
В конвейере отчёт junit подхватывают системы CI, а --silent убирает вывод console.log из логов, не отключая сами тесты. Если сборка большая, разделите её на независимые задания и запускайте тесты по каталогам — это проще, чем пытаться распараллелить один процесс.
Типичные ошибки
- Тесты падают на импорте CSS или картинок — проверьте, что конфиг Vitest использует те же плагины, что и сборка.
- Не разрешаются алиасы вида
@/— перенеситеresolve.aliasв общий конфиг, а не дублируйте в двух файлах. - Окружение
jsdomне установлено — добавьте пакет в devDependencies, иначе получите ошибку при старте. - Моки «протекают» между тестами — включите
restoreMocksи очищайте глобальные подмены. - Процесс не завершается после прогона — ищите открытые таймеры и соединения; помогает
pool: 'forks'и корректное закрытие ресурсов. - Снапшоты устарели после рефакторинга — обновляйте их осознанно через
vitest -u, а не автоматически в CI.
npx vitest run src/lib/price.test.ts
npx vitest -u
npx vitest --reporter=verbose
npx vitest --help
Локальный запуск одного файла экономит минуты при отладке, а подробный репортер показывает длительность каждого теста. Если тест нестабилен, сначала проверьте общие состояния и реальные таймеры, а не увеличивайте ожидания.
Итог
Vitest даёт быстрый старт без отдельного транспайлера, знакомый API и покрытие из коробки. Минимальный набор — установить пакеты, описать окружение, написать несколько тестов на чистые функции и подключить прогон в CI. Дальше расширяйте покрытие по модулям, которые действительно ломаются, и следите за чистотой моков: именно они чаще всего превращают полезные тесты в хрупкую декорацию.