Зачем и какие тесты
Тест это код, который проверяет код и падает, если поведение сломалось. Главная ценность: смелость менять код без страха.
- Unit: одна функция или модуль в изоляции. Быстрые (миллисекунды), их много.
- Интеграционные: несколько частей вместе: компонент с хуками и стором, сервис с базой.
- E2E (end-to-end): настоящий браузер кликает по собранному приложению. Медленные и дорогие, зато ловят то, что видит пользователь.
Пирамида тестов: много unit, меньше интеграционных, немного e2e. Во фронтенде популярен вариант «трофей»: упор на интеграционные тесты компонентов, потому что они ближе к реальному использованию и всё ещё быстрые.
Инструменты 2026
- Vitest: раннер для проектов на Vite, API почти как у Jest (
describe,it,expect,vi.fn). Jest по-прежнему живёт в старых проектах. - Testing Library: рендерит компонент и ищет элементы так, как это делает пользователь: по роли, тексту, label.
- MSW (Mock Service Worker): перехват сетевых запросов на уровне сети, а не подмена fetch в каждом тесте.
- Playwright: e2e в Chromium, Firefox и WebKit, с автоожиданием элементов.
Структура теста: Arrange, Act, Assert
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
it('увеличивает счётчик по клику', async () => {
render(<Counter />); // Arrange
await userEvent.click(screen.getByRole('button', { name: '+' })); // Act
expect(screen.getByText('Счёт: 1')).toBeInTheDocument(); // Assert
});
Тестируй поведение, а не реализацию: что видит и делает пользователь, а не внутренний state или имена приватных методов. Тогда рефакторинг не ломает тесты.
Запросы Testing Library
| Метод | Не нашёл | Когда |
|---|---|---|
getBy... | бросает ошибку | элемент должен быть сейчас |
queryBy... | возвращает null | проверить, что элемента нет |
findBy... | промис, ждёт появления (по умолчанию до 1 с) | элемент появится асинхронно |
Приоритет: getByRole и getByLabelText лучше, чем getByTestId, потому что заодно проверяют доступность.
Моки и тестовые двойники
- stub: подставная реализация с заранее заданным ответом;
- spy: обёртка, которая запоминает вызовы (с какими аргументами, сколько раз);
- mock: двойник с ожиданиями, которые проверяются в тесте.
const onSave = vi.fn();
render(<Form onSave={onSave} />);
// ...
expect(onSave).toHaveBeenCalledWith({ name: 'Аня' });
vi.mock('./api') подменяет модуль целиком, вызов поднимается (hoisting) в начало файла до импортов. Мокать стоит границы системы (сеть, время, случайность), а не всё подряд: тест, где замокано всё, проверяет моки.
Время подделывают через vi.useFakeTimers(), чтобы тестировать debounce и таймауты без реального ожидания.
Покрытие
Coverage показывает, какие строки выполнялись в тестах, но не то, что их результат проверен. 100% покрытия без хороших expect ничего не гарантирует. Это ориентир, а не цель.
Флаки-тесты
Тест, который то проходит, то падает без изменений кода. Причины: гонки и фиксированные sleep, зависимость от порядка тестов и общего состояния, реальное время и сеть. Лечится ожиданием условия (findBy, автоожидание Playwright), изоляцией данных и фейковыми таймерами, а не увеличением таймаутов.
TDD
Цикл red, green, refactor: написать падающий тест, минимальный код до зелёного, затем улучшить код под защитой теста.
Частые ошибки
- Тестировать реализацию: state, вызовы внутренних функций.
getByдля проверки отсутствия элемента (бросит ошибку раньшеexpect).setTimeout/sleepв тестах вместо ожидания условия.- Гнаться за процентом покрытия.
Что спросят на собесе
- Пирамида тестов и чем unit отличается от интеграционного и e2e.
- Что такое моки, стабы и шпионы, что стоит мокать.
- Как тестировать React-компонент и почему по ролям.
- Откуда берутся флаки-тесты и как их лечить.
- Что даёт и чего не даёт coverage.