ReactMiddleTechnical

Как тестировать React-компоненты? В чём разница между unit, integration и e2e тестированием?

Unit-тесты проверяют изолированные функции/компоненты, integration — взаимодействие нескольких частей вместе, e2e — весь пользовательский сценарий в реальном браузере. Для React типичный стек: Vitest + React Testing Library (unit/integration) и Playwright (e2e).

Три уровня тестирования

  • Unit: одна функция, хук или компонент изолированно. Зависимости мокируются. Быстро, дёшево, хрупко при рефакторинге внутренностей.
  • Integration: несколько компонентов/модулей в связке. Реальный DOM через jsdom, реальная логика, только внешние сервисы (API) мокируются. Баланс скорости и уверенности.
  • E2E: реальный браузер, реальный бэкенд (или MSW-заглушка). Медленно, но проверяет весь путь пользователя.

Инструменты

  • Vitest — быстрый test runner совместимый с Jest API, нативно понимает ESM и Vite-конфиг.
  • React Testing Library (RTL) — рендерит компоненты в jsdom, поощряет запросы через роли/текст, а не через CSS-классы.
  • MSW (Mock Service Worker) — перехватывает fetch/XHR на уровне Service Worker или Node; одни и те же хендлеры работают в браузере и в тестах.
  • Playwright — e2e в Chromium/Firefox/WebKit, поддерживает трассировку, видеозапись, CI-режим.

Unit-тест: хук useCounter

// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('increment increases count', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

Integration-тест: форма с API-запросом

// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { LoginForm } from './LoginForm';

const server = setupServer(
  http.post('/api/login', () =>
    HttpResponse.json({ token: 'abc' })
  )
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('submits credentials and shows welcome', async () => {
  render(<LoginForm />);
  await userEvent.type(screen.getByLabelText('Email'), 'user@test.com');
  await userEvent.type(screen.getByLabelText('Password'), 'secret');
  await userEvent.click(screen.getByRole('button', { name: /войти/i }));
  expect(await screen.findByText(/добро пожаловать/i)).toBeInTheDocument();
});

E2E-тест на Playwright

// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: /войти/i }).click();
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Добро пожаловать')).toBeVisible();
});

Когда что применять

  • Чистая бизнес-логика, утилиты, хуки → unit.
  • Форма с валидацией + вызов API → integration с MSW.
  • Критичный пользовательский путь (регистрация, оплата) → e2e.

Подводные камни

  • Тест через getByTestId привязывает тест к вёрстке, а не к поведению — предпочитайте getByRole, getByLabelText.
  • Мок реализации, а не контракта: если мокируете внутренний модуль, тест не поймает смену API.
  • Забытый await в RTL: асинхронные запросы надо ждать через findBy* или waitFor, иначе тест проходит зелёным, не дождавшись ответа.
  • Нет server.resetHandlers(): обработчики MSW «протекают» между тестами.
  • Слишком много e2e: медленные, нестабильные на CI. Пирамида тестирования: много unit, меньше integration, мало e2e.
  • act() warnings: обновления состояния вне act вызывают предупреждения и могут скрывать реальные баги.
  • jsdom не поддерживает layout: CSS-позиционирование, getBoundingClientRect, resize-события работают неправильно — для этого нужен e2e.

Common mistakes

  • Тестировать имплементацию (state, имена хуков) вместо поведения.
  • Мокать всю сеть в integration-тестах и пропускать ошибки сериализации.
  • Гонять e2e на каждый push и убивать CI-бюджет.

What the interviewer is testing

  • Различает ли цели уровней тестов.
  • Знает ли актуальные библиотеки (RTL, Vitest, MSW, Playwright).
  • Понимает ли принцип «testing user behavior».

Sources

Related topics