React Testing Library (RTL) стала стандартом для тестирования компонентов в React благодаря акценту на взаимодействие с приложением так, как это делает пользователь. Эффективное использование RTL требует понимания не только API библиотеки, но и принципов построения устойчивых и читаемых тестов.
Ключевая идея RTL — проверка того, как компонент ведет себя для пользователя, а не его внутренней структуры. Не стоит ориентироваться на внутренние методы компонентов или конкретные классы.
Пример неправильного подхода:
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
test('проверяет наличие кнопки по классу', () => {
const { container } = render(<MyComponent />);
const button = container.querySelector('.my-button');
expect(button).toBeInTheDocument();
});
Правильный подход с использованием RTL:
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
test('отображает кнопку с текстом "Отправить"', () => {
render(<MyComponent />);
const button = screen.getByRole('button', { name: /отправить/i });
expect(button).toBeInTheDocument();
});
Использование getByRole,
getByLabelText,
getByText делает тесты более устойчивыми к
изменениям структуры и CSS.
RTL предоставляет утилиты для имитации действий пользователя:
fireEvent и userEvent. userEvent
более подходящий вариант для эмуляции настоящего взаимодействия, так как
учитывает задержки и последовательность событий.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyForm from './MyForm';
test('ввод текста в поле и отправка формы', async () => {
render(<MyForm />);
const input = screen.getByLabelText(/имя/i);
await userEvent.type(input, 'Алексей');
const submit = screen.getByRole('button', { name: /отправить/i });
await userEvent.click(submit);
expect(screen.getByText(/привет, Алексей/i)).toBeInTheDocument();
});
Использование userEvent обеспечивает реалистичность
тестов и минимизирует ложноположительные результаты.
Компоненты часто делают асинхронные запросы или обновляют состояние с
задержкой. В таких случаях необходимо использовать методы
findBy* или
waitFor.
import { render, screen, waitFor } from '@testing-library/react';
import FetchData from './FetchData';
test('отображает данные после загрузки', async () => {
render(<FetchData />);
const item = await screen.findByText(/данные загружены/i);
expect(item).toBeInTheDocument();
});
findBy* автоматически ожидает появления элемента в
течение таймаута, что делает тесты более надежными.
waitFor позволяет оборачивать произвольные проверки:
await waitFor(() => expect(screen.getByText(/успех/i)).toBeVisible());
Моки нужны, но их избыточное использование усложняет тесты и делает их менее реалистичными. Следует мокировать только сторонние зависимости, такие как API-запросы или сторонние сервисы.
jest.mock('./api', () => ({
fetchData: jest.fn(() => Promise.resolve({ name: 'Тест' }))
}));
Местные функции и стейт компонентов лучше оставлять нетронутыми, проверяя их косвенно через интерфейс.
Тесты должны быть самодокументируемыми. Рекомендуется:
describe для логической
группировки.// Arrange
render(<MyComponent />);
// Act
await userEvent.click(screen.getByText(/отправить/i));
// Assert
expect(screen.getByText(/успешно/i)).toBeInTheDocument();
Эта структура улучшает восприятие и поддержку тестов.
RTL автоматически очищает DOM после каждого теста. Однако, при использовании глобальных моков или нестандартных контейнеров важно убедиться, что нет остаточного состояния, которое может влиять на другие тесты.
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
});
Снимки полезны для статических компонентов, но в сложных интерфейсах с динамическими данными они часто становятся источником ложных срабатываний. Лучшей практикой является проверка конкретного поведения или контента.
Для форм важно тестировать не только визуальное отображение, но и
валидацию и взаимодействие. Использование
userEvent совместно с getByLabelText позволяет
эмулировать поведение пользователя максимально близко.
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.click(screen.getByRole('button', { name: /отправить/i }));
expect(screen.getByText(/спасибо/i)).toBeInTheDocument();
RTL рекомендует:
getByRole для кнопок, чекбоксов,
ссылокgetByLabelText для полей вводаgetByText для статического текстаgetByPlaceholderText только если нет
альтернативgetByTestId как последний вариант,
если элемент нельзя выбрать иначеДля компонентов, использующих контексты или Redux, лучше оборачивать компонент в провайдеры в тесте, а не мокировать состояние полностью. Это делает тесты ближе к реальной эксплуатации.
import { render, screen } from '@testing-library/react';
import { MyContextProvider } from './context';
import MyComponent from './MyComponent';
render(
<MyContextProvider value={{ user: 'Алексей' }}>
<MyComponent />
</MyContextProvider>
);
expect(screen.getByText(/Алексей/i)).toBeInTheDocument();
Если проект на TypeScript, стоит типизировать события, мокированные функции и состояния компонентов. Это предотвращает ошибки на этапе написания тестов и повышает надежность.
Эти практики формируют устойчивую основу для тестирования компонентов React. С их соблюдением тесты становятся читаемыми, поддерживаемыми и ориентированными на реальное поведение интерфейса.