Навигация

Стратегия тестирования фронтенда

Хорошая стратегия тестирования помогает уверенно изменять код, не утопая в хрупких тестах и медленном CI. В статье разбираем, как выбирать уровни тестов, когда использовать doubles, как управлять рисками и что измерять. Принципы универсальны для любого SPA-фреймворка; для SSR/fullstack (Next.js/Nuxt) отдельно разбираем серверный слой.

Содержание

  1. От пирамиды к реальности
  2. Как выбрать уровень теста
  3. Test Doubles: dummy, fake, stub, spy, mock
  4. Shift-left и Shift-right
  5. Risk-based testing: тестировать по рискам
  6. Стратегия для фреймворков (SPA и SSR)
  7. Метрики качества тестирования
  8. Архитектура тестового кода
  9. План внедрения в существующий проект
  10. Чеклист стратегии
  11. Ключевые тезисы для интервью
  12. Заключение
  13. Полезные ссылки

От пирамиды к реальности

Ты уже знаешь пирамиду тестирования и Testing Trophy. Но на практике одной модели мало.

МодельКогда применимаОпасность
Пирамида КонаСерверные приложения, библиотеки утилитПереоценка unit-тестов компонентов
Testing TrophyReact/Next/Vue SPAМожно недооценить E2E для критичных сценариев
Ice Cream Cone«Тестируем только через UI»Медленный CI, flaky-тесты
Test HoneycombМикросервисы, API-зависимые фронтыМного контрактных тестов

Testing Trophy в цифрах для SPA-проекта

Статические проверки (TypeScript, ESLint, lint-staged)  ──  бесплатная защита
Unit-тесты (утилиты, хуки, чистая логика)              ──  ~20–30%
Интеграционные тесты (компоненты + MSW)                ──  ~50–60%
E2E-тесты (критичные пользовательские сценарии)        ──  ~10–20%
Визуальное регрессионное тестирование                   ──  по необходимости

💡 Главное правило: чем ближе тест к пользователю, тем выше уверенность, но дороже поддержка. Не экономь на unit, но не пытайся покрыть ими всё.


Как выбрать уровень теста

Для каждой фичи задай четыре вопроса:

  1. Насколько критична функция? (авторизация vs настройка темы)
  2. Как часто она меняется? (часто меняющийся UI vs стабильная валидация)
  3. Сколько интеграций затрагивает? (один хук vs полный checkout)
  4. Какова цена ошибки? (некликабельная кнопка vs утечка данных)

Матрица принятия решений

ФункцияУровеньПочему
Валидация emailUnitЧистая функция, много edge cases
Хук useDebounceUnit + интеграционныйЛогика + интеграция с lifecycle фреймворка
Форма логинаИнтеграционныйКомпонент + валидация + API
Прохождение оплатыE2EМного страниц, критично для бизнеса
Отображение списка товаровИнтеграционный + визуальноеДанные + рендер + вёрстка
Адаптивная вёрсткаВизуальноеСкриншоты лучше проверяют UI

Test Doubles: dummy, fake, stub, spy, mock

В тестах редко используются реальные зависимости. Вместо них применяют doubles — заменители.

ТипЧто делаетПример
DummyПросто заполняет параметр, не используетсяПустой объект User в тесте сортировки
FakeРаботает упрощённо, но реальноIn-memory репозиторий вместо PostgreSQL
StubВозвращает заранее заданный ответvi.fn().mockReturnValue({ id: 1 })
SpyСледит за вызовами, сохраняет поведениеvi.spyOn(console, "log")
MockПолностью контролирует поведение и проверяет вызовыvi.mock("./api")

Когда что использовать

// Dummy — нужен только чтобы компилятор не ругался
const dummyUser: User = { id: "", name: "", email: "" };

// Stub — фиксированный ответ
const stub = vi.fn().mockReturnValue(42);

// Spy — проверяем, что метод вызван
const spy = vi.spyOn(api, "fetchUser").mockResolvedValue({ id: 1 });

// Mock — полностью заменяем модуль
vi.mock("./api", () => ({ fetchUser: vi.fn() }));

Shift-left и Shift-right

Shift-left — тестировать раньше

Чем раньше найдена ошибка, тем дешевле её исправить.

ЭтапЧто делать
Написание кодаTDD, типизация, линтинг
КоммитPre-commit hooks: lint, typecheck, unit-тесты изменённых файлов
PRПолный прогон unit + интеграционных тестов
MergeE2E smoke, визуальные тесты

Shift-right — тестировать в production

Некоторые проблемы невозможно поймать в staging: реальные данные, нагрузка, сетевые условия.

  • Мониторинг ошибок (Sentry)
  • Feature flags для канареечных релизов
  • Smoke-тесты на production
  • Real User Monitoring (RUM) для метрик производительности

💡 Идеальный баланс: максимум проверок сдвинуть влево, но оставить observability справа.


Risk-based testing: тестировать по рискам

У тебя ограниченное время. Трать его туда, где ошибка обойдётся дороже всего.

Классификация по риску

РискПримерПриоритет
ВысокийОплата, авторизация, удаление данныхE2E + интеграционные + unit
СреднийФильтры, поиск, сортировкаИнтеграционные + unit
НизкийТекстовые подсказки, анимацииВизуальное / ручная проверка

Практический пример

Фича: Корзина покупок
├─ Добавление товара в корзину      → интеграционный тест
├─ Изменение количества             → интеграционный тест
├─ Расчёт итоговой суммы            → unit + интеграционный
├─ Применение промокода             → unit + интеграционный
└─ Оформление заказа → оплата       → E2E (критичный сценарий)

Стратегия для фреймворков (SPA и SSR)

1. Статические проверки — первая линия обороны

  • TypeScript ловит целые классы ошибок до запуска тестов.
  • ESLint предотвращает глупые баги: для React — плагины react-hooks, jsx-a11y, для Vue — eslint-plugin-vue.
  • Prettier убирает шум в diff.

Настрой lint-staged, чтобы проверки проходили до коммита:

// .lintstagedrc
{
  "*.{ts,tsx}": ["eslint --fix", "prettier --write", "tsc --noEmit"]
}

2. Unit-тесты — фундамент

Тестируй:

  • утилиты и хелперы,
  • валидаторы,
  • мапперы данных,
  • кастомные хуки (React) и composables (Vue) в изоляции,
  • редьюсеры и чистую бизнес-логику.

Избегай unit-тестов компонентов с десятками моков — это проверка моков, не кода.

3. Интеграционные тесты компонентов — основная масса

Инструмент зависит от фреймворка, принцип один: тестируй поведение пользователя на реальных зависимостях. Для React — React Testing Library + MSW, для Vue — Vue Test Utils или @testing-library/vue + MSW, для Nuxt — mountSuspended + registerEndpoint из @nuxt/test-utils.

it("displays user name after loading", async () => {
  server.use(
    http.get("/api/user/1", () => HttpResponse.json({ name: "Alice" }))
  );

  render(<UserProfile userId={1} />);

  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

4. E2E — только критичные сценарии

Не пытайся покрыть E2E каждый экран. Выбирай сценарии, где:

  • задействовано несколько страниц,
  • ошибка критична для бизнеса,
  • интеграционный тест не даёт достаточной уверенности.

5. Визуальное тестирование

Подходит для:

  • компонентов дизайн-системы,
  • страниц с богатым UI,
  • проверки адаптивности.

Инструменты: Chromatic, Percy, Playwright toHaveScreenshot.

6. Контрактное тестирование

Если фронтенд активно общается с бэкендом, рассмотри Pact или OpenAPI + тесты на соответствие схеме. Это ловит расхождения в API до интеграции.

7. Особенности SSR / fullstack (Next.js / Nuxt)

Пункты 1–6 описывают стратегию для клиентских SPA. В Next.js/Nuxt появляется серверный слой, и стратегия меняется в четырёх местах:

  1. Серверный код тестируется как unit-функции. Server Actions, Route Handlers, middleware и Server Components — это обычные функции. Их вызывают напрямую с сконструированным FormData/Request и проверяют результат и побочные эффекты (revalidatePath, redirect), не запуская HTTP-сервер.

  2. Загрузка данных уходит на сервер. В SPA данные грузятся в хуке после render(). В SSR useFetch/useAsyncData (Nuxt) и Server Components (Next.js) грузят данные на сервере во время рендера — простой render() не увидит их. Для таких компонентов нужен SSR-рантайм: mountSuspended + registerEndpoint в Nuxt, вызов Server Component как async-функции с рендером полученного JSX в Next.js.

  3. Мокируется больше фреймворк-API. next/navigation, next/headers, next/cache, runtime config и auto-imports Nuxt не существуют вне рантайма — их мокают системно в setup-файле, а не в каждом тесте.

  4. Доля unit-тестов растёт за счёт серверного слоя. К цепочке «интеграционные компоненты + unit для логики» добавляются unit-тесты Server Actions, Route Handlers и middleware. E2E по-прежнему только для критичных сценариев.


Метрики качества тестирования

Покрытие кода — не единственная и не самая важная метрика.

МетрикаЧто показываетЦелевое значение
Code coverageДоля кода, выполняемого тестами> 70–80% для критичных модулей
Flaky rateДоля нестабильных тестов< 1%
Test execution timeВремя прогона CI< 10 минут для PR
MTTR (Mean Time To Repair)Время починки упавшего CI< 30 минут
Escape rateБаги, пропущенные в productionснижать квартал к кварталу
Test maintenance timeВремя на поддержку тестов< 20% от времени разработки

⚠️ Ловушка: погоня за 100% покрытием часто порождает бесполезные тесты. Лучше 70% качественного покрытия, чем 100% ради галочки.


Архитектура тестового кода

Тесты — тоже код. Их нужно проектировать.

Фабрики тестовых данных

// src/test/factories.ts
export const createUser = (overrides?: Partial<User>): User => ({
  id: crypto.randomUUID(),
  name: "Test User",
  email: "test@example.com",
  role: "user",
  ...overrides,
});

Page Object Model для E2E

// e2e/pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto("/login");
  }

  async login(email: string, password: string) {
    await this.page.getByLabel("Email").fill(email);
    await this.page.getByLabel("Password").fill(password);
    await this.page.getByRole("button", { name: /login/i }).click();
  }
}

Разделение тестовых окружений

src/test/
  setup.ts          # глобальная настройка Vitest
  mocks/
    handlers.ts     # базовые MSW-handlers
    server.ts       # setupServer
  factories.ts      # фабрики данных
  utils.tsx         # renderWithProviders

План внедрения в существующий проект

Не пытайся покрыть всё сразу. Действуй итерациями:

Этап 1. Защита критичных мест (1–2 недели)

  • Выбери 3–5 самых важных бизнес-сценариев.
  • Напиши на них E2E-тесты.
  • Настрой CI, чтобы они запускались на каждый PR.

Этап 2. Интеграционные тесты компонентов (2–4 недели)

  • Подключи MSW.
  • Покрой основные пользовательские потоки: авторизация, формы, списки.

Этап 3. Unit-тесты для логики (постоянно)

  • Тестируй утилиты, хуки, редьюсеры.
  • Добавляй тест при фиксе бага.

Этап 4. Оптимизация (постоянно)

  • Мониторь время CI.
  • Убирай flaky-тесты.
  • Пересматривай соотношение уровней.

Чеклист стратегии

  • Определены критичные бизнес-сценарии, которые покрыты E2E.
  • Для компонентов преобладают интеграционные тесты, а не unit с моками.
  • Используются фабрики данных, а не хардкод в каждом тесте.
  • MSW мокирует API на сетевом уровне.
  • E2E тестируют поведение пользователя, а не внутренние состояния.
  • CI проходит за приемлемое время (< 10–15 минут на PR).
  • Есть метрики: coverage, flaky rate, время прогона.
  • Тесты не проверяют реализацию, а проверяют контракт.
  • Каждый найденный баг сначала воспроизводится тестом, потом чинится.

Ключевые тезисы для интервью

  • Чем ближе тест к пользователю, тем выше уверенность, но дороже поддержка; для SPA-приложений основа — интеграционные тесты, unit для чистой логики, E2E для критичных сценариев.
  • В SSR/fullstack (Next.js/Nuxt) серверный код — Server Actions, Route Handlers, middleware — тестируется как unit-функции, а компоненты с серверной загрузкой данных требуют SSR-рантайма (mountSuspended/вызов Server Component как функции), что увеличивает долю unit-тестов.
  • Выбор уровня теста зависит от критичности функции, частоты изменений, количества интеграций и цены ошибки; risk-based testing направляет усилия на сценарии с высокой ценой ошибки.
  • Test doubles (dummy, fake, stub, spy, mock) — разные заменители зависимостей; shift-left сдвигает проверки к моменту написания кода, shift-right добавляет observability в production.
  • Статические проверки (TypeScript, ESLint) — первая линия обороны; MSW мокирует API на сетевом уровне, делая интеграционные тесты реалистичными.
  • Покрытие кода не равно качеству тестов; важнее flaky rate, время прогона и escape rate; фабрики данных и Page Object Model упрощают поддержку и делают тесты стабильнее.
  • Внедрять стратегию тестирования лучше итерациями: сначала защитить критичные сценарии, затем наращивать интеграционные и unit-тесты, не перегружая CI.

Заключение

Стратегия тестирования — это баланс между уверенностью и затратами. Не нужно гнаться за 100% покрытием или покрывать E2E каждый экран: важно выбирать правильный уровень под задачу, управлять рисками и следить за метриками. Для SPA-приложений оптимальная основа — статические проверки, интеграционные тесты с MSW, unit-тесты для логики и E2E для критичных пользовательских потоков. В SSR/fullstack (Next.js/Nuxt) к этому добавляется unit-покрытие серверного слоя — Server Actions, Route Handlers и middleware. Начинайте с защиты самых важных сценариев и улучшайте процесс постепенно.

Полезные ссылки