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вместоuserEvent—fireEvent.click()не симулирует реальное поведение пользователя;userEvent.click()включает hover, focus, keyboard events- Запрос элемента до render —
screen.getByText(...)внеrenderвызова вернёт ошибку actwarning — возникает при обновлении state вне userEvent; используйawait userEvent.*илиawait waitFor
Связанные темы
- _MOC Тестирование
- Testing Library -- подход
- Тестирование рендеринга
- Тестирование форм
- Тестирование хуков
- Jest основы