React Testing Library: основы

React Testing Library (RTL) — библиотека для тестирования React-компонентов через DOM с позиции пользователя: запросы по тексту, роли и label, а не по CSS-классам и структуре дерева.

Зачем нужно

Enzyme тестировал внутреннее состояние (state, props, lifecycle). RTL поощряет тестировать то, что видит и делает пользователь — это делает тесты устойчивыми к рефакторингу. Философия: «чем больше тесты напоминают реальное использование, тем больше им можно доверять».

Где используется

  • Тестирование React-компонентов в Jest/Vitest
  • Проверка рендеринга, взаимодействий, асинхронных обновлений
  • Любые React-проекты: CRA, Vite, Next.js

Основной контент

Установка

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
// jest.setup.ts
import '@testing-library/jest-dom';

Базовый тест компонента

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

test('вызывает onClick при нажатии', async () => {
  const handleClick = jest.fn;
  render(<Button onClick={handleClick}>Нажми меня</Button>);

  const button = screen.getByRole('button', { name: 'Нажми меня' });
  await userEvent.click(button);

  expect(handleClick).toHaveBeenCalledTimes(1);
});

Запросы (queries) — приоритет

// 1. getByRole — лучший вариант (accessibility first)
screen.getByRole('button', { name: 'Submit' });
screen.getByRole('textbox', { name: 'Email' });
screen.getByRole('heading', { name: 'Заголовок', level: 1 });

// 2. getByLabelText — для полей формы
screen.getByLabelText('Email адрес');

// 3. getByPlaceholderText
screen.getByPlaceholderText('Введите email');

// 4. getByText — для не-интерактивных элементов
screen.getByText('Привет, мир');

// 5. getByTestId — последний приоритет (только если нет другого способа)
screen.getByTestId('user-avatar');

Асинхронный тест

// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

jest.mock('./api', () => ({
  getUser: jest.fn.mockResolvedValue({ name: 'Alice', email: 'alice@example.com' }),
}));

test('показывает данные пользователя после загрузки', async () => {
  render(<UserProfile userId={1} />);

  expect(screen.getByText('Загрузка...')).toBeInTheDocument;

  await screen.findByText('Alice'); // waitFor встроен в findBy*

  expect(screen.getByText('alice@example.com')).toBeInTheDocument;
});

Тестирование форм

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

test('отправляет форму с данными', async () => {
  const onSubmit = jest.fn;
  render(<LoginForm onSubmit={onSubmit} />);

  await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
  await userEvent.type(screen.getByLabelText('Пароль'), 'secret123');
  await userEvent.click(screen.getByRole('button', { name: 'Войти' }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: 'user@example.com',
    password: 'secret123',
  });
});

Полезные матчеры jest-dom

expect(element).toBeInTheDocument;
expect(element).toBeVisible;
expect(element).toBeDisabled;
expect(element).toHaveValue('text');
expect(element).toHaveTextContent('Hello');
expect(element).toHaveClass('active');
expect(element).toHaveFocus;

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

  • Использование getByTestId везде — нарушает accessibility-first подход; используй getByRole/getByLabelText в первую очередь
  • fireEvent вместо userEventfireEvent.click() не симулирует реальное поведение пользователя; userEvent.click() включает hover, focus, keyboard events
  • Запрос элемента до renderscreen.getByText(...) вне render вызова вернёт ошибку
  • act warning — возникает при обновлении state вне userEvent; используй await userEvent.* или await waitFor

Связанные темы

Ресурсы