Стратегия тестирования фронтенда
Хорошая стратегия тестирования помогает уверенно изменять код, не утопая в хрупких тестах и медленном CI. В статье разбираем, как выбирать уровни тестов, когда использовать doubles, как управлять рисками и что измерять. Принципы универсальны для любого SPA-фреймворка; для SSR/fullstack (Next.js/Nuxt) отдельно разбираем серверный слой.
Содержание
- От пирамиды к реальности
- Как выбрать уровень теста
- Test Doubles: dummy, fake, stub, spy, mock
- Shift-left и Shift-right
- Risk-based testing: тестировать по рискам
- Стратегия для фреймворков (SPA и SSR)
- Метрики качества тестирования
- Архитектура тестового кода
- План внедрения в существующий проект
- Чеклист стратегии
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
От пирамиды к реальности
Ты уже знаешь пирамиду тестирования и Testing Trophy. Но на практике одной модели мало.
| Модель | Когда применима | Опасность |
|---|---|---|
| Пирамида Кона | Серверные приложения, библиотеки утилит | Переоценка unit-тестов компонентов |
| Testing Trophy | React/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, но не пытайся покрыть ими всё.
Как выбрать уровень теста
Для каждой фичи задай четыре вопроса:
- Насколько критична функция? (авторизация vs настройка темы)
- Как часто она меняется? (часто меняющийся UI vs стабильная валидация)
- Сколько интеграций затрагивает? (один хук vs полный checkout)
- Какова цена ошибки? (некликабельная кнопка vs утечка данных)
Матрица принятия решений
| Функция | Уровень | Почему |
|---|---|---|
| Валидация email | Unit | Чистая функция, много edge cases |
Хук useDebounce | Unit + интеграционный | Логика + интеграция с 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 + интеграционных тестов |
| Merge | E2E 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 появляется серверный слой, и стратегия меняется в четырёх местах:
-
Серверный код тестируется как unit-функции. Server Actions, Route Handlers, middleware и Server Components — это обычные функции. Их вызывают напрямую с сконструированным
FormData/Requestи проверяют результат и побочные эффекты (revalidatePath,redirect), не запуская HTTP-сервер. -
Загрузка данных уходит на сервер. В SPA данные грузятся в хуке после
render(). В SSRuseFetch/useAsyncData(Nuxt) и Server Components (Next.js) грузят данные на сервере во время рендера — простойrender()не увидит их. Для таких компонентов нужен SSR-рантайм:mountSuspended+registerEndpointв Nuxt, вызов Server Component как async-функции с рендером полученного JSX в Next.js. -
Мокируется больше фреймворк-API.
next/navigation,next/headers,next/cache, runtime config и auto-imports Nuxt не существуют вне рантайма — их мокают системно в setup-файле, а не в каждом тесте. -
Доля 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. Начинайте с защиты самых важных сценариев и улучшайте процесс постепенно.