Unit тестирование с Vitest — это не просто ещё одна библиотека для проверки кода, это удобный инструмент, который делает процесс тестирования живым и предсказуемым. В этой статье я расскажу, как начать работать с Vitest, какие приёмы ускоряют разработку и на что стоит обратить внимание при переходе от других тест-раннеров. Материал рассчитан на практиков: примеры, советы и реальные подводные камни из моего опыта внедрения тестов в проектах на Vue и React.

Почему стоит выбрать Vitest

Vitest возник как логичное продолжение экосистемы Vite: он быстрый, поддерживает модульную систему ESM и работает почти как родной инструмент для современных фронтенд-проектов. В отличие от некоторых более тяжёлых решений, Vitest загружает тесты быстро и в режиме наблюдения реагирует на изменения без лишней перезагрузки окружения.

По личным наблюдениям, команда привыкает к Vitest быстрее, чем к альтернативам: конфиг прост, API знакомо выглядит, а инструменты для мокирования и асинхронных сценариев покрывают большую часть повседневных задач. Это не значит, что у него нет ограничений, но многие из них легко обойти при грамотной настройке.

Установка и базовая настройка

Установить Vitest можно парой команд, если ваш проект уже использует npm или yarn. Для большинства случаев достаточно добавить пакет и настроить скрипты в package.json. В проектах на Vite подключение происходит быстро, а для других сборщиков доступно несколько примеров конфигураций.

Ниже приведены минимальные шаги для установки в проект с npm.

npm install -D vitest @vitest/ui
# или
yarn add -D vitest @vitest/ui

Далее в package.json добавьте скрипты для запуска тестов и режима наблюдения. Для интеграции с Vite можно использовать отдельный конфиг или поместить настройки в vite.config.ts, если требуется специфическая трансформация модулей.

Ключевые возможности и API

Основные конструкции Vitest знакомы разработчикам, привыкшим к Jest: функции describe, it, expect работают ожидаемо. Кроме этого есть встроенные утилиты для мокирования (vi), управление таймерами и запуск тестов в режиме watch, что ускоряет цикл разработки.

Вот краткий список того, что будет использоваться чаще всего:

  • describe / it / test — структура и сами тестовые случаи;
  • expect — ассерты с богатым набором матчеров;
  • vi.fn, vi.spyOn, vi.mock — средства для мокирования и контроля вызовов;
  • fake timers и контроль времени для тестирования debounce/throttle;
  • snapshot-тестирование и запуск в режиме watch для быстрого обратного отклика.

Пишем первые юнит-тесты: пример на функции

Для начала рассмотрим простой пример: утилитная функция, которая форматирует строки. Тесты помогут зафиксировать поведение при граничных значениях и при изменении кода в будущем. Пример ниже показывает структуру файла и базовые проверки.

// format.js
export function capitalize(str) {
  if (!str) return '';
  return str[0].toUpperCase() + str.slice(1);
}

// format.test.js
import { describe, it, expect } from 'vitest';
import { capitalize } from './format';

describe('capitalize', () => {
  it('преобразует первую букву в заглавную', () => {
    expect(capitalize('hello')).toBe('Hello');
  });

  it('возвращает пустую строку при пустом вводе', () => {
    expect(capitalize('')).toBe('');
  });
});

Такой подход помогает быстро обнаружить регрессии и делает код удобным для рефакторинга. В реальном проекте тесты пишутся рядом с модулями, это упрощает навигацию и поддержку.

Мокирование и изоляция: vi в действии

В проектах часто нужно изолировать модуль от внешних зависимостей: HTTP-запросов, локального хранилища или больших утилит. Vitest предоставляет vi.mock и vi.fn, которые работают похоже на привычные мок-объекты, но учитывают особенности ESM. Это делает моки гибкими и предсказуемыми.

Пример использования vi.mock для подмены HTTP-клиента.

// api.js
export async function fetchUser(id) {
  const res = await fetch(`/users/${id}`);
  return res.json();
}

// api.test.js
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './api';

vi.stubGlobal('fetch', vi.fn(() =>
  Promise.resolve({ json: () => ({ id: 1, name: 'Anna' }) })
));

describe('fetchUser', () => {
  it('возвращает данные пользователя', async () => {
    const user = await fetchUser(1);
    expect(user.name).toBe('Anna');
  });
});

Важно аккуратно управлять состоянием глобальных моков между тестами, чтобы не получить ложные срабатывания. У меня в старых проектах одна забытая глобальная подмена приводила к неочевидным тестовым сбоям — поэтому всегда очищаю моки в afterEach.

Асинхронный код и контроль времени

Тестирование асинхронных функций и таймеров — частая боль, особенно когда используется setTimeout, debounce или polling. Vitest умеет работать с fake timers, позволяя продвигать время вперёд и проверять поведение без реальных задержек. Это экономит время при запуске набора тестов и делает результаты стабильными.

Пример использования fake timers для debounce-функции.

import { vi, describe, it, expect, afterEach } from 'vitest';
import { debounce } from './debounce';

describe('debounce', () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  it('вызов колбека один раз при быстрой серии событий', () => {
    vi.useFakeTimers();
    const fn = vi.fn();
    const deb = debounce(fn, 200);

    deb();
    deb();
    deb();

    vi.advanceTimersByTime(200);
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

Такой тест надёжен и не зависит от реального времени. При использовании fake timers важно возвращать реальное время в afterEach, чтобы не нарушить другие тесты.

Интеграция с CI и рекомендации по структуре

Автоматический запуск тестов в CI — обязательный элемент зрелого процесса. Vitest легко интегрируется в популярные CI-системы: достаточно вызвать npm run test или настроить специфичные флаги для запуска в headless-режиме. Для ускорения сборки тестов применяйте параллелизм и кеширование, когда это возможно.

Ниже — краткий перечень рекомендаций по организации тестовой базы и CI.

  • разделяйте unit и интеграционные тесты по папкам для гибкого запуска;
  • используйте теги или паттерны файлов, чтобы запускать критичные тесты в PR-ах;
  • чистите и сбрасывайте глобальное состояние между тестами;
  • включайте отчёты покрытия кода и отслеживайте их динамику в CI.

Типичные ошибки и как их избежать

Частая ошибка — тестирование реализации вместо поведения. Это приводит к ломким тестам, которые падают при рефакторинге, хотя логика не изменилась. Лучше проверять внешнее поведение модуля и его контракты, а не внутренние детали.

Ещё одна проблема — смешение unit и интеграционных тестов в одном наборе. Если тест зависит от реального сетевого слоя или базы данных, он перестаёт быть быстрым и надёжным. Для таких случаев лучше явно пометить тесты и запускать их отдельно в CI.

Мой опыт внедрения Vitest в рабочий проект

В одном из проектов мы начали с миграции небольшого набора утилит и компонентов на Vitest, чтобы проверить, как команда воспримет новое окружение. Первые несколько дней были посвящены настройке конфигурации и написанию шаблонов для тестов, затем процесс пошёл быстрее и продуктивность выросла.

Практическая польза проявилась через пару недель: баги при рефакторинге начали обнаруживаться на ранних этапах, ревью стали короче, а разработчики стали реже бояться правок в критических модулях. Накопленный опыт показал, что важнее стабильные, небольшие тесты, чем громоздкие сценарии с множеством зависимостей.

Краткие практические советы

Ниже собраны полезные приёмы, которые сэкономят время при ежедневной работе с Vitest.

Задача Рекомендация
Быстрый feedback Используйте режим watch с фильтрацией по имени теста
Изоляция Мокируйте сетевые запросы и внешние модули
Надёжность Очистка стора и восстановление таймеров в afterEach

И ещё одно наблюдение: не старайтесь покрыть все подряд мелкие геттеры тестами сразу, начните с критичных сценариев и добавляйте покрытия туда, где вероятность регрессии высока. Такой подход экономит время и увеличивает отдачу от тестов.

Vitest — современный инструмент, который укладывается в привычные рабочие процессы и даёт гибкость, нужную для реальных проектов. Он не идеален, но сочетание скорости, простоты и совместимости с ESM делает его отличным выбором для большинства фронтенд-команд.

Если вы только начинаете или планируете миграцию наборов тестов, начните с небольшого пилотного набора, настройте CI и постепенно расширяйте покрытие, опираясь на реальные баги и точки роста в проекте. Такой путь позволит избежать перегрузки команды и получить ощутимую пользу от тестирования в короткие сроки.