React Testing Library: тестирование компонентов глазами пользователя
Для кого эта статья:
- Разработчики фронтенд-приложений, использующие React
- Люди, желающие улучшить навыки тестирования и качества кода
Специалисты и команды, стремящиеся внедрить практики тестирования в свои проекты
Тестирование фронтенд-приложений – область, где каждый разработчик сталкивается с выбором инструментария. React Testing Library разработана специально для решения ключевой проблемы: создания надёжных тестов, имитирующих поведение реальных пользователей. В отличие от тестирования изолированных функций и состояний, RTL заставляет вас смотреть на приложение глазами людей, которые будут им пользоваться. Эта философия коренным образом меняет подход к качеству кода и устойчивости всего приложения. 🧪
Хотите овладеть мастерством тестирования React-приложений и стать востребованным разработчиком? Программа Обучение веб-разработке от Skypro включает подробный модуль по React Testing Library с реальными проектами под руководством действующих разработчиков. Наши выпускники пишут тесты, которые действительно ловят баги до релиза – навык, высоко ценимый в каждой технической команде.
Что такое React Testing Library и ее философия
React Testing Library (RTL) — это набор утилит, предоставляющих виртуальное DOM-окружение для тестирования React-компонентов. RTL не является полноценным тестовым фреймворком, а скорее дополнением к таким инструментам, как Jest, Mocha или Jasmine, предоставляя API для взаимодействия с React-компонентами в тестах.
Ключевая философия RTL заключена в её слогане: "Чем больше ваши тесты напоминают способ использования вашего программного обеспечения, тем больше уверенности они могут вам дать". Иными словами, RTL побуждает тестировать то, что видит пользователь, а не внутренние механизмы компонентов. 🔍
Алексей Морозов, Lead Frontend Developer
Когда я впервые столкнулся с React Testing Library, наша команда использовала Enzyme для тестирования React-компонентов. Мы постоянно сталкивались с проблемой, когда тесты проходили успешно, но приложение всё равно ломалось. Причина была в том, что мы тестировали внутреннее состояние компонентов и их методы, а не то, что реально видит пользователь.
Переход на RTL был нетривиальным — пришлось переписать более 200 тестов. Однако результаты превзошли ожидания: количество "ложноположительных" тестов сократилось на 70%, а новые тесты стали намного проще в поддержке. Самое главное — теперь, когда пользователи сообщают о проблемах, наши тесты действительно их обнаруживают, а не проходят успешно, игнорируя реальные ошибки в интерфейсе.
В отличие от других библиотек тестирования React, таких как Enzyme, RTL не предоставляет доступ к экземплярам компонентов или их состоянию. Вместо этого, RTL позволяет взаимодействовать с компонентами так, как это делают пользователи: находить элементы по тексту, ярлыкам, ролям или атрибутам доступности, кликать по кнопкам и ссылкам, вводить текст в поля ввода и проверять результаты этих действий.
| Подход | React Testing Library | Enzyme |
|---|---|---|
| Философия тестирования | Тестирование пользовательского опыта | Тестирование реализации компонентов |
| Доступ к props и state | Нет прямого доступа | Полный доступ |
| Устойчивость к рефакторингу | Высокая (тесты не ломаются при изменении внутренней структуры) | Низкая (тесты часто нужно переписывать) |
| Основной фокус | Доступность и восприятие пользователями | Внутренняя структура и логика компонентов |
Этот подход обеспечивает несколько ключевых преимуществ:
- Тесты становятся более устойчивыми к рефакторингу, поскольку они не привязаны к деталям реализации
- Фокус на доступности (accessibility) поощряет создание более инклюзивных интерфейсов
- Тесты точнее отражают реальное использование приложения
- Упрощение поддержки тестов, так как они меньше зависят от внутренних деталей компонентов

Основные принципы и преимущества RTL
React Testing Library построена вокруг нескольких ключевых принципов, которые отражают её подход к тестированию фронтенд-приложений:
- Тестирование поведения, а не реализации. RTL фокусируется на тестировании того, что компоненты делают, а не как они это делают. Это означает, что тесты не должны знать о внутренней структуре компонентов.
- Приоритет доступности. Библиотека поощряет поиск элементов по ролям ARIA, ярлыкам и текстовому содержимому — тем же способом, которым пользователи взаимодействуют с приложением.
- Минимальные предположения. RTL предоставляет только необходимые инструменты для тестирования, избегая соблазна тестировать всё подряд.
- Удобочитаемые ошибки. Когда тест не проходит, RTL предоставляет подробные и понятные сообщения об ошибках, которые помогают быстро выявить проблему.
Основные преимущества использования React Testing Library включают:
- 💪 Устойчивость к рефакторингу: тесты не ломаются при изменении внутренней структуры компонентов, если внешнее поведение остаётся прежним
- 🧠 Интуитивность: тесты пишутся с точки зрения пользователя, что делает их более понятными и естественными
- ♿ Улучшение доступности: RTL поощряет разработчиков учитывать вопросы доступности при создании компонентов
- ⚡ Простота и лаконичность: API библиотеки минималистичен и прост в использовании
- 🔄 Поддержка асинхронных операций: библиотека предлагает удобные методы для работы с асинхронными событиями
Сравним RTL с другими популярными решениями для тестирования:
| Характеристика | React Testing Library | Enzyme | Jest DOM |
|---|---|---|---|
| Основной подход | Тестирование с точки зрения пользователя | Тестирование состояний и реализации | Расширение Jest для работы с DOM |
| Сложность настройки | Низкая | Средняя | Низкая (часть RTL) |
| Интеграция с React Hooks | Встроенная | Требует дополнительных настроек | Не предназначен для этого |
| Кривая обучения | Пологая | Крутая | Пологая |
| Поддержка сообщества | Высокая и растущая | Стабильная, но стагнирующая | Высокая (как часть экосистемы Jest) |
Настройка среды для работы с React Testing Library
Настройка React Testing Library для работы с вашим React-проектом — это относительно простой процесс, особенно если вы используете Create React App, который уже включает в себя все необходимые зависимости. Однако, если вы настраиваете проект с нуля или добавляете RTL в существующий проект, вам понадобится выполнить несколько шагов.
Для начала, установите необходимые пакеты:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Где:
- @testing-library/react — основная библиотека для тестирования React-компонентов
- @testing-library/jest-dom — расширения для Jest с дополнительными матчерами для DOM
- @testing-library/user-event — библиотека для имитации действий пользователя
После установки пакетов создайте файл конфигурации Jest (jest.config.js), если он еще не создан:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
}
};
Если вы используете TypeScript, убедитесь, что у вас установлены типы для библиотек тестирования:
npm install --save-dev @types/testing-library__react @types/testing-library__jest-dom
Для эффективного тестирования я рекомендую создать вспомогательный файл setup, который будет содержать общие утилиты и настройки. Например, создайте файл src/setupTests.js:
// Импортируем расширения Jest DOM для дополнительных матчеров
import '@testing-library/jest-dom';
// Устанавливаем уровень логирования
import { configure } from '@testing-library/react';
configure({ testIdAttribute: 'data-testid' });
// Здесь вы можете добавить глобальные моки для fetch, localStorage и т.д.
Для проектов с маршрутизацией (React Router) вам также понадобится специальная настройка. Создайте вспомогательную функцию для рендеринга компонентов с маршрутизацией:
// src/test-utils.js
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
// Пользовательский рендерер
export const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return {
...render(ui, { wrapper: BrowserRouter }),
};
};
Для компонентов, использующих Redux, создайте аналогичную утилиту:
// src/test-utils.js
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './your-reducers-path';
export function renderWithRedux(
ui,
{ initialState, store = createStore(rootReducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
};
}
После этих настроек вы готовы писать тесты с использованием React Testing Library. Каждый тест обычно следует структуре "Arrange-Act-Assert" (Подготовка-Действие-Проверка):
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('should increment counter on click', () => {
// Arrange
render(<Counter />);
// Act
fireEvent.click(screen.getByText(/increment/i));
// Assert
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
Практические методы тестирования с помощью RTL
Используя React Testing Library, вы можете эффективно тестировать различные аспекты поведения ваших React-компонентов. Рассмотрим основные методы и подходы, которые помогут вам создавать надёжные и эффективные тесты. 🧪
Марина Соколова, Frontend Team Lead
В моей команде мы долго спорили о подходах к тестированию: одни разработчики предпочитали TDD (Test-Driven Development), другие считали его излишне трудозатратным. Переход на React Testing Library стал компромиссным решением.
Мы начали с простого правила: каждый новый компонент должен иметь базовый тест рендеринга и как минимум один тест взаимодействия. На удивление, разработчики, изначально скептически относившиеся к тестированию, стали добровольно расширять тестовое покрытие. Причина оказалась в том, что RTL делает тесты похожими на пользовательские сценарии, которые понятны даже не-техническим специалистам.
Через три месяца количество регрессий при выпуске новых версий снизилось на 65%, а команда начала воспринимать тестирование не как бремя, а как инструмент, помогающий писать лучший код. RTL изменила саму культуру разработки в нашей команде.
1. Рендеринг компонентов
Основой любого теста с RTL является рендеринг компонента:
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders component', () => {
const { container } = render(<MyComponent />);
// Дальнейшее тестирование...
});
Функция render возвращает объект с несколькими полезными свойствами:
container– DOM-элемент, в который был рендерен компонентgetByText,getByRole,getByLabelTextи другие методы для поиска элементовrerender– метод для повторного рендеринга компонента с новыми propsunmount– метод для размонтирования компонента
2. Поиск элементов
RTL предоставляет различные методы для поиска элементов в DOM. Они делятся на три категории в зависимости от их поведения при отсутствии элемента:
getBy*– возвращает элемент или выбрасывает исключение, если элемент не найденqueryBy*– возвращает элемент или null, если элемент не найденfindBy*– возвращает Promise, который разрешается в элемент, когда он появляется в DOM
Каждая категория включает различные методы поиска элементов:
// По тексту
const element = screen.getByText('Hello, world');
// По роли (ARIA role) и имени
const button = screen.getByRole('button', { name: /submit/i });
// По атрибуту data-testid
const input = screen.getByTestId('username-input');
// По тексту метки (label)
const passwordInput = screen.getByLabelText('Password');
// По placeholder
const searchInput = screen.getByPlaceholderText('Search...');
// По значению
const selectedOption = screen.getByDisplayValue('Option 1');
3. Имитация взаимодействия пользователя
Для имитации действий пользователя RTL предоставляет два основных модуля: fireEvent и userEvent. Рекомендуется использовать userEvent, поскольку он более точно имитирует реальное поведение пользователя:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits form with username and password', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
// Ввод текста
await userEvent.type(screen.getByLabelText(/username/i), 'testuser');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
// Клик по кнопке
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
// Проверки...
});
4. Проверка результатов
Для проверки результатов тестов RTL использует матчеры Jest, расширенные матчерами @testing-library/jest-dom:
// Проверка наличия элемента в DOM
expect(screen.getByText('Welcome back!')).toBeInTheDocument();
// Проверка, что кнопка отключена
expect(screen.getByRole('button')).toBeDisabled();
// Проверка видимости элемента
expect(screen.getByTestId('error-message')).toBeVisible();
// Проверка значения input
expect(screen.getByLabelText('Email')).toHaveValue('test@example.com');
// Проверка атрибута
expect(screen.getByRole('checkbox')).toBeChecked();
// Проверка содержимого элемента
expect(screen.getByTestId('user-info')).toHaveTextContent('John Doe');
// Проверка стиля
expect(screen.getByText('Error')).toHaveStyle('color: red');
5. Тестирование асинхронного поведения
Для тестирования асинхронного поведения, такого как загрузка данных или отложенный рендеринг, используйте методы findBy* и waitFor:
test('loads and displays user data', async () => {
render(<UserProfile userId="123" />);
// Проверка отображения состояния загрузки
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Ожидание загрузки данных
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
// Или с использованием waitFor для более сложных условий
await waitFor(() => {
expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
});
});
6. Моки и шпионы
Используйте моки и шпионы Jest для изоляции тестируемых компонентов от внешних зависимостей:
test('calls onSubmit with form data', async () => {
const mockSubmit = jest.fn();
render(<ContactForm onSubmit={mockSubmit} />);
await userEvent.type(screen.getByLabelText('Name'), 'Test User');
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(mockSubmit).toHaveBeenCalledWith({
name: 'Test User',
email: 'test@example.com'
});
});
Реальные примеры тестов React-компонентов с RTL
Рассмотрим несколько реальных примеров тестирования типичных React-компонентов с использованием React Testing Library. Эти примеры охватывают различные сценарии, с которыми вы скорее всего столкнётесь в своих проектах. 📝
Пример 1: Тестирование формы авторизации
Предположим, у нас есть форма авторизации с полями для электронной почты и пароля, кнопкой входа и обработкой ошибок:
// LoginForm.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits correct credentials', async () => {
const mockLogin = jest.fn();
render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
test('shows validation errors for empty fields', async () => {
render(<LoginForm onLogin={jest.fn()} />);
// Пытаемся отправить пустую форму
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
// Проверяем, что появились сообщения об ошибках
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
// Убеждаемся, что функция onLogin не была вызвана
expect(onLogin).not.toHaveBeenCalled();
});
test('displays server error message', async () => {
// Имитируем серверную ошибку
const mockLogin = jest.fn().mockRejectedValue(new Error('Invalid credentials'));
render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrongpassword');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
// Проверяем отображение ошибки
const errorMessage = await screen.findByText(/invalid credentials/i);
expect(errorMessage).toBeInTheDocument();
});
Пример 2: Тестирование компонента списка задач с фильтрацией
Представим компонент TodoList, который отображает список задач, позволяет добавлять новые задачи, отмечать их выполненными и фильтровать по статусу:
// TodoList.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoList from './TodoList';
test('adds new todo item', async () => {
render(<TodoList />);
const input = screen.getByPlaceholderText(/add new task/i);
await userEvent.type(input, 'Buy groceries{enter}');
// Проверяем, что новая задача добавлена
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
});
test('marks todo item as completed', async () => {
render(<TodoList initialTodos={[{ id: 1, text: 'Write tests', completed: false }]} />);
// Находим чекбокс рядом с задачей и нажимаем на него
const checkbox = screen.getByRole('checkbox', { name: /write tests/i });
await userEvent.click(checkbox);
// Проверяем, что задача помечена как выполненная
expect(checkbox).toBeChecked();
expect(screen.getByText('Write tests')).toHaveStyle('text-decoration: line-through');
});
test('filters todo items', async () => {
const todos = [
{ id: 1, text: 'Task 1', completed: false },
{ id: 2, text: 'Task 2', completed: true },
{ id: 3, text: 'Task 3', completed: false }
];
render(<TodoList initialTodos={todos} />);
// Изначально все задачи видны
expect(screen.getAllByRole('listitem')).toHaveLength(3);
// Фильтруем только активные задачи
await userEvent.click(screen.getByRole('button', { name: /active/i }));
const activeItems = screen.getAllByRole('listitem');
expect(activeItems).toHaveLength(2);
expect(screen.getByText('Task 1')).toBeInTheDocument();
expect(screen.getByText('Task 3')).toBeInTheDocument();
expect(screen.queryByText('Task 2')).not.toBeInTheDocument();
// Фильтруем только выполненные задачи
await userEvent.click(screen.getByRole('button', { name: /completed/i }));
const completedItems = screen.getAllByRole('listitem');
expect(completedItems).toHaveLength(1);
expect(screen.getByText('Task 2')).toBeInTheDocument();
});
Пример 3: Тестирование компонента с асинхронной загрузкой данных
Предположим, у нас есть компонент UserProfile, который загружает данные пользователя с сервера:
// UserProfile.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';
// Имитируем сервер с помощью MSW (Mock Service Worker)
const server = setupServer(
rest.get('/api/user/123', (req, res, ctx) => {
return res(
ctx.json({
id: 123,
name: 'John Doe',
email: 'john@example.com',
bio: 'Frontend developer'
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('loads and displays user data', async () => {
render(<UserProfile userId="123" />);
// Проверяем, что отображается индикатор загрузки
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Ожидаем загрузки данных
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
// Проверяем отображаемые данные
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByText('Frontend developer')).toBeInTheDocument();
});
test('shows error message on API failure', async () => {
// Переопределяем обработчик для имитации ошибки
server.use(
rest.get('/api/user/123', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: 'Server error' }));
})
);
render(<UserProfile userId="123" />);
// Ожидаем исчезновения индикатора загрузки
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
// Проверяем сообщение об ошибке
expect(screen.getByText(/error loading user data/i)).toBeInTheDocument();
});
test('allows editing user profile', async () => {
render(<UserProfile userId="123" isEditable={true} />);
// Ожидаем загрузки данных
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
// Нажимаем на кнопку редактирования
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
// Редактируем имя пользователя
const nameInput = screen.getByLabelText(/name/i);
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'Jane Doe');
// Сохраняем изменения
await userEvent.click(screen.getByRole('button', { name: /save/i }));
// Проверяем, что новое имя отображается
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
При написании тестов с помощью React Testing Library следуйте этим рекомендациям:
- Фокусируйтесь на пользовательском опыте и взаимодействии, а не на внутренних деталях реализации
- Используйте наиболее специфичные и стабильные селекторы (предпочтительный порядок: роли с именами → текст → тестовые идентификаторы)
- Используйте
userEventвместоfireEventдля более реалистичной имитации взаимодействий - Применяйте принцип "изоляции" тестов: каждый тест должен быть независим от других
- Тестируйте только публичное API компонентов, а не их внутренние методы
- Уделяйте особое внимание тестированию доступности ваших компонентов
- Избегайте чрезмерного тестирования очевидного поведения React
React Testing Library кардинально меняет подход к тестированию фронтенд-приложений, заставляя разработчиков взглянуть на свой код глазами пользователей. Следуя философии "тестируй так, как пользователь взаимодействует с приложением", вы создаете не просто тесты, а гарантию того, что пользовательский опыт останется высоким при любых изменениях в кодовой базе. Начните с малого — протестируйте один ключевой компонент вашего приложения, и вы увидите, насколько проще становится поддерживать и развивать ваш код, когда у вас есть надежная система автоматических тестов.
Читайте также
- 8 отечественных аналогов Notion и Trello: импортозамещение в IT
- Framer Motion в React: плавные анимации без головной боли
- Allure Framework: создание информативных отчетов о тестировании
- Как создать QR-код: инструменты, настройка, дизайн и применение
- Полное руководство по тестированию SOAP API: инструменты, методы
- Microsoft Project: управление задачами, ресурсами, сроками – полный гид
- Системные требования OBS и OCCT: подготовка ПК для стриминга
- Топ-10 инструментов разработчика: от текстовых редакторов до CI/CD
- Лучшие платформы для обратного проектирования: выбор инструментов
- Как создать чат-бота в Telegram: пошаговая инструкция для новичков


