Vitest - это Vite-native test runner: в одной поставке assertions, моки, снапшоты и coverage, отдельная библиотека утверждений не нужна. Ключевое отличие от других раннеров в том, что он гоняет тесты через тот же Vite pipeline, что и приложение: тот же transform, те же плагины, те же алиасы. Поэтому Vitest сразу понимает ESM, TypeScript, JSX и виртуальные модули проекта - отдельную систему трансформации для тестов строить не приходится. API при этом близко к Jest (expect, describe, test), так что переучиваться почти не нужно.
Сам раннер ставится одной dev-зависимостью, но рядом сразу нужны ещё две. Поддержка покрытия в Vitest опциональна и живёт отдельным пакетом - для провайдера по умолчанию это @vitest/coverage-v8, без него скрипт с флагом --coverage не отработает. Плагин vite-tsconfig-paths подтягивает пути из tsconfig, на него опирается конфиг ниже. В package.json полезно завести три скрипта. Команда vitest без аргументов включает watch в интерактивном терминале и перезапускает только затронутые тесты, опираясь на граф зависимостей Vite - изменил модуль, перепрогнались лишь тесты, которые его касаются. Команда vitest run выполняет ровно один проход и завершается. В CI всегда используйте явный run-скрипт: watch там просто повиснет и заблокирует пайплайн.
Первый конфиг живёт в vitest.config.ts или в секции test общего Vite config, и его типизирует defineConfig из vitest/config. Начинать стоит с минимума: среда, маска файлов и пара правил про моки. Среда по умолчанию - node, и для серверного кода этого достаточно; DOM-среду (jsdom или более лёгкий happy-dom) подключают только там, где она реально нужна, и лучше не глобально, а на уровне отдельного проекта или файла.
Каждое поле секции test читается как решение, а не как ритуал. environment задаёт среду исполнения. include - какие файлы считаются тестами. globals: false оставляет describe и expect явными импортами, что проще типизировать и читать; включённые globals экономят строку импорта ценой неявности. clearMocks сбрасывает историю вызовов между тестами, restoreMocks возвращает оригиналы подменённым методам - обе про изоляцию, о которой отдельная глава. coverage выбирает провайдер (v8 по умолчанию) и границы отчёта.
Полезно один раз свести основные опции и то, что каждая приносит, чтобы не держать это в голове и не копировать чужой конфиг вслепую. Ниже - короткая карта полей, к которым возвращаешься чаще всего.
| Опция | Зачем | Не путать |
|---|---|---|
| environment | Среда: node / jsdom / happy-dom | Browser Mode настраивается отдельно |
| setupFiles | Код перед каждым тест-файлом | Он обязан быть идемпотентным |
| globals | Глобальные describe/expect | Явные импорты проще типизировать |
| resolve.alias | Общие Vite-алиасы | TS paths требуют плагин |
| pool | threads / forks / vmThreads | Меняйте по изоляции и native-модулям |
Vite-native подход выгоден именно тем, что тесты видят ровно тот код, который поедет в прод: один и тот же transform, а не его приблизительная копия. Алиасы, tsconfig paths (через плагин vite-tsconfig-paths), JSX и виртуальные модули не приходится описывать второй конфигурацией и держать её в синхронизации с первой. Меньше конфигурации - меньше расхождений между тем, как код собирается для приложения и как для тестов.
Отсюда правило: держите конфиг минимальным. Каждая глобальная настройка влияет на весь suite и способна тихо связать тесты с неявной средой - тогда они зелёные на вашей машине и красные в CI или у коллеги. Добавляйте опцию, только когда можете объяснить, какую конкретную проблему она решает, и по возможности задавайте узкое поведение ближе к тесту, а не глобально на всех.
Типичный провал - выставить jsdom глобально на всякий случай. Серверным тестам браузерная эмуляция не нужна: она медленнее стартует и хуже отражает реальный runtime, в котором код действительно работает с файловой системой, сетью и процессом. Держите по умолчанию node, а jsdom или happy-dom включайте адресно там, где под тестом настоящий DOM, - позже это станет отдельным Test Project со своей средой.
npm i -D vitest @vitest/coverage-v8 vite-tsconfig-paths
# package.json
# "scripts": {
# "test": "vitest",
# "test:run": "vitest run",
# "test:coverage": "vitest run --coverage"
# }import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: 'node',
include: ['src/**/*.test.{ts,tsx}'],
globals: false,
clearMocks: true,
restoreMocks: true,
coverage: { provider: 'v8' },
},
})