Урок по треку React 7 мин

Тестирование React-компонентов

Testing Library, act, моки и что именно стоит проверять.

На собесе по тестам проверяют три вещи: что именно ты тестируешь, какими инструментами и умеешь ли писать тесты, которые не ломаются от рефакторинга.

Что тестировать

Тестируйте поведение, которое видит пользователь, а не детали реализации. Пользователь не знает про useState и имена классов: он видит текст, кнопки, поля и результат своих действий.

Плохой тест проверяет, что состояние isOpen стало true. Хороший: что после клика по кнопке «Меню» появился список с пунктами. Первый сломается при переходе на useReducer, второй нет.

Популярная модель «кубок тестирования»: основа это статический анализ (TypeScript, ESLint), дальше немного юнит-тестов чистой логики, больше всего интеграционных тестов компонентов вместе с детьми, и несколько E2E на ключевые сценарии.

Инструменты

  • Vitest (или Jest): раннер тестов, expect, моки. В новых проектах на Vite чаще Vitest. Окружение DOM даёт jsdom или happy-dom.
  • React Testing Library (RTL): рендерит компонент и ищет элементы так, как это делал бы пользователь.
  • @testing-library/user-event: реалистичные действия пользователя.
  • @testing-library/jest-dom: матчеры toBeInTheDocument, toBeDisabled, toHaveTextContent (работают и в Vitest).
  • MSW: перехват запросов на уровне сети, чтобы не мокать fetch руками.
  • Playwright: E2E в настоящих браузерах.

react-test-renderer в React 19 объявлен устаревшим, Enzyme не поддерживает современный React. Новые тесты на них не пишут.

Первый тест

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('добавляет задачу', async () => {
  const user = userEvent.setup();
  render(<TodoApp />);

  await user.type(screen.getByRole('textbox', { name: 'Новая задача' }), 'Купить хлеб');
  await user.click(screen.getByRole('button', { name: 'Добавить' }));

  expect(screen.getByRole('listitem')).toHaveTextContent('Купить хлеб');
});

Методы user-event асинхронные, их нужно ждать через await.

Запросы: get, query, find

ПрефиксНет элементаНесколькоКогда
getByбросает ошибкуошибкаэлемент должен быть сейчас
queryByвозвращает nullошибкапроверить, что элемента нет
findByпромис отклоняется после таймаутаошибкаэлемент появится асинхронно

У каждого есть вариант All для списков. findBy это waitFor плюс getBy, таймаут по умолчанию 1000 мс.

expect(screen.queryByText('Ошибка')).not.toBeInTheDocument();
expect(await screen.findByText('Готово')).toBeInTheDocument();

Приоритет запросов

  1. getByRole с name: так элементы видят скринридеры. Заодно проверяется доступность.
  2. getByLabelText для полей формы.
  3. getByPlaceholderText, getByText, getByDisplayValue.
  4. getByAltText, getByTitle.
  5. getByTestId в последнюю очередь, когда иначе никак.

Если элемент не находится по роли, часто это сигнал, что разметка недоступна (например, div вместо button).

Асинхронность и act

RTL оборачивает render и события в act, поэтому вручную его почти не вызывают. Предупреждение «not wrapped in act» обычно значит, что тест закончился, а компонент ещё обновляется: нужно дождаться результата через findBy или waitFor.

await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));

В waitFor кладут только проверку: колбэк вызывается много раз, побочные действия там выполнятся многократно.

В React 19 act импортируют из react, старый путь react-dom/test-utils удалён.

Сеть и хуки

Запросы мокают через MSW: обработчики описывают ответы API, компонент делает настоящий fetch. Тест не знает, каким клиентом пользуется код, и переживёт замену axios на fetch.

Кастомный хук тестируют через renderHook из @testing-library/react, но если хук используется в одном компоненте, проще проверить компонент.

const { result } = renderHook(() => useCounter());
act(() => result.current.inc());
expect(result.current.count).toBe(1);

Таймеры

Для debounce и таймеров включают фейковые часы (vi.useFakeTimers()). user-event нужно сообщить о них: userEvent.setup({ advanceTimers: vi.advanceTimersByTime }), иначе действия зависнут.

Частые ошибки

  • Проверка внутреннего состояния и вызова приватных функций.
  • getBy для проверки отсутствия (он бросит ошибку раньше expect).
  • Забытый await перед user.click или findBy.
  • Повсеместный data-testid вместо ролей.
  • Снапшоты огромных деревьев: их обновляют не глядя, и они ничего не ловят.

Что спросят на собесе

  • Что тестировать в компоненте и чего не тестировать?
  • Чем отличаются getBy, queryBy и findBy?
  • Как замокать запросы к API в тестах компонентов?
  • Почему RTL советует искать по роли?
  • Как тестировать асинхронный код и что значит предупреждение про act?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.