React Testing Library: тестирование компонентов глазами пользователя

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики фронтенд-приложений, использующие 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 построена вокруг нескольких ключевых принципов, которые отражают её подход к тестированию фронтенд-приложений:

  1. Тестирование поведения, а не реализации. RTL фокусируется на тестировании того, что компоненты делают, а не как они это делают. Это означает, что тесты не должны знать о внутренней структуре компонентов.
  2. Приоритет доступности. Библиотека поощряет поиск элементов по ролям ARIA, ярлыкам и текстовому содержимому — тем же способом, которым пользователи взаимодействуют с приложением.
  3. Минимальные предположения. RTL предоставляет только необходимые инструменты для тестирования, избегая соблазна тестировать всё подряд.
  4. Удобочитаемые ошибки. Когда тест не проходит, 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:

JS
Скопировать код
// Импортируем расширения Jest DOM для дополнительных матчеров
import '@testing-library/jest-dom';

// Устанавливаем уровень логирования
import { configure } from '@testing-library/react';
configure({ testIdAttribute: 'data-testid' });

// Здесь вы можете добавить глобальные моки для fetch, localStorage и т.д.

Для проектов с маршрутизацией (React Router) вам также понадобится специальная настройка. Создайте вспомогательную функцию для рендеринга компонентов с маршрутизацией:

JS
Скопировать код
// 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, создайте аналогичную утилиту:

JS
Скопировать код
// 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" (Подготовка-Действие-Проверка):

JS
Скопировать код
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 является рендеринг компонента:

JS
Скопировать код
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders component', () => {
const { container } = render(<MyComponent />);
// Дальнейшее тестирование...
});

Функция render возвращает объект с несколькими полезными свойствами:

  • container – DOM-элемент, в который был рендерен компонент
  • getByText, getByRole, getByLabelText и другие методы для поиска элементов
  • rerender – метод для повторного рендеринга компонента с новыми props
  • unmount – метод для размонтирования компонента

2. Поиск элементов

RTL предоставляет различные методы для поиска элементов в DOM. Они делятся на три категории в зависимости от их поведения при отсутствии элемента:

  • getBy* – возвращает элемент или выбрасывает исключение, если элемент не найден
  • queryBy* – возвращает элемент или null, если элемент не найден
  • findBy* – возвращает Promise, который разрешается в элемент, когда он появляется в DOM

Каждая категория включает различные методы поиска элементов:

JS
Скопировать код
// По тексту
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, поскольку он более точно имитирует реальное поведение пользователя:

JS
Скопировать код
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:

JS
Скопировать код
// Проверка наличия элемента в 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:

JS
Скопировать код
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 для изоляции тестируемых компонентов от внешних зависимостей:

JS
Скопировать код
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: Тестирование формы авторизации

Предположим, у нас есть форма авторизации с полями для электронной почты и пароля, кнопкой входа и обработкой ошибок:

JS
Скопировать код
// 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, который отображает список задач, позволяет добавлять новые задачи, отмечать их выполненными и фильтровать по статусу:

JS
Скопировать код
// 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, который загружает данные пользователя с сервера:

JS
Скопировать код
// 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 следуйте этим рекомендациям:

  1. Фокусируйтесь на пользовательском опыте и взаимодействии, а не на внутренних деталях реализации
  2. Используйте наиболее специфичные и стабильные селекторы (предпочтительный порядок: роли с именами → текст → тестовые идентификаторы)
  3. Используйте userEvent вместо fireEvent для более реалистичной имитации взаимодействий
  4. Применяйте принцип "изоляции" тестов: каждый тест должен быть независим от других
  5. Тестируйте только публичное API компонентов, а не их внутренние методы
  6. Уделяйте особое внимание тестированию доступности ваших компонентов
  7. Избегайте чрезмерного тестирования очевидного поведения React

React Testing Library кардинально меняет подход к тестированию фронтенд-приложений, заставляя разработчиков взглянуть на свой код глазами пользователей. Следуя философии "тестируй так, как пользователь взаимодействует с приложением", вы создаете не просто тесты, а гарантию того, что пользовательский опыт останется высоким при любых изменениях в кодовой базе. Начните с малого — протестируйте один ключевой компонент вашего приложения, и вы увидите, насколько проще становится поддерживать и развивать ваш код, когда у вас есть надежная система автоматических тестов.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какова основная цель использования React Testing Library?
1 / 5

Загрузка...