Антон Ларичев

Введение
React Testing Library (RTL) — библиотека для тестирования React-компонентов с точки зрения пользователя. В отличие от Enzyme, RTL не тестирует детали реализации: внутреннее состояние, методы жизненного цикла или структуру компонентного дерева. Философия проста: тесты должны имитировать то, как пользователь взаимодействует с приложением.
Вместе с Jest RTL становится мощным инструментом для написания надёжных тестов, которые не ломаются при рефакторинге.
Установка и настройка
Если вы используете Create React App или Next.js, React Testing Library уже включена в зависимости. Для ручной установки:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Добавьте импорт матчеров jest-dom в файл настройки тестов:
// jest.setup.js
import '@testing-library/jest-dom';
И укажите его в конфигурации Jest:
// jest.config.js
module.exports = {
setupFilesAfterFramework: ['./jest.setup.js'],
};
Основные концепции: запросы и приоритеты
RTL предоставляет несколько типов запросов для поиска элементов. Выбор правильного запроса — ключ к качественным тестам.
Приоритет запросов
getByRole— самый предпочтительный, отражает доступность интерфейсаgetByLabelText— для полей формыgetByText— для текстового содержимогоgetByTestId— последний вариант, когда ничего другого не подходит
Варианты запросов
Каждый запрос существует в трёх вариантах: getBy бросает ошибку при отсутствии элемента, queryBy возвращает null, findBy возвращает Promise и используется для асинхронных сценариев.
Простой пример тестирования кнопки:
// Button.jsx
function Button({ onClick, children, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
}
// Button.test.jsx
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('отображает текст кнопки', () => {
render(<Button>Отправить</Button>);
// ищем по роли и имени — доступность проверяется автоматически
expect(screen.getByRole('button', { name: 'Отправить' })).toBeInTheDocument();
});
test('кнопка заблокирована при disabled', () => {
render(<Button disabled>Отправить</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
Тестирование пользовательских взаимодействий
Для симуляции действий пользователя используйте @testing-library/user-event — он точнее воспроизводит реальное поведение браузера по сравнению с fireEvent.
// Counter.jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>Счётчик: {count}</span>
<button onClick={() => setCount(c => c + 1)}>Увеличить</button>
<button onClick={() => setCount(c => c - 1)}>Уменьшить</button>
</div>
);
}
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('увеличивает счётчик при клике', async () => {
// userEvent.setup() создаёт экземпляр с корректной очередью событий
const user = userEvent.setup();
render(<Counter />);
const incrementBtn = screen.getByRole('button', { name: 'Увеличить' });
await user.click(incrementBtn);
expect(screen.getByText('Счётчик: 1')).toBeInTheDocument();
});
Асинхронные тесты
Когда компонент загружает данные асинхронно, используйте findBy* или waitFor.
// UserProfile.jsx
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Загрузка...</p>;
return <h1>{user.name}</h1>;
}
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';
beforeEach(() => {
// мокаем глобальный fetch перед каждым тестом
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Иван Петров' }),
})
);
});
test('отображает имя пользователя после загрузки', async () => {
render(<UserProfile userId={1} />);
// проверяем состояние загрузки
expect(screen.getByText('Загрузка...')).toBeInTheDocument();
// findBy автоматически ждёт появления элемента в DOM
const heading = await screen.findByRole('heading', { name: 'Иван Петров' });
expect(heading).toBeInTheDocument();
});
Тестирование форм
Формы — один из самых частых сценариев. Проверяем ввод и вызов обработчика сабмита:
// LoginForm.jsx
function LoginForm({ onSubmit }) {
return (
<form onSubmit={e => {
e.preventDefault();
const data = new FormData(e.target);
onSubmit({ email: data.get('email'), password: data.get('password') });
}}>
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Пароль
<input type="password" name="password" required />
</label>
<button type="submit">Войти</button>
</form>
);
}
// LoginForm.test.jsx
test('вызывает onSubmit с данными формы', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
// ищем поля по тексту лейбла — проверяет корректность атрибута for/htmlFor
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Пароль'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Войти' }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123',
});
});
Частые ошибки
Поиск по data-testid вместо семантических запросов
Плохо:
// хрупкий тест, не проверяет доступность
const btn = screen.getByTestId('submit-btn');
Хорошо:
// проверяет и функцию, и доступность интерфейса
const btn = screen.getByRole('button', { name: 'Отправить' });
Ручное использование act() без необходимости
RTL автоматически оборачивает большинство операций в act(). Если вы добавляете его вручную — это сигнал, что вы боретесь с инфраструктурой тестов, а не решаете реальную проблему.
Предупреждения об обновлениях состояния после unmount
Если тест завершается с предупреждением Warning: An update to ... inside a test was not wrapped in act(...), асинхронная операция продолжается после окончания теста. Используйте waitFor или findBy и убедитесь, что все промисы завершаются до конца теста.
Тестирование деталей реализации
Не проверяйте state, внутренние методы или структуру компонентного дерева. Тест должен ломаться только тогда, когда ломается поведение с точки зрения пользователя, — не при рефакторинге.
Заключение
React Testing Library помогает писать тесты, которые проверяют реальное поведение приложения, а не детали реализации. Ключевые принципы: использовать семантические запросы, имитировать действия пользователя через userEvent, правильно обрабатывать асинхронность через findBy и waitFor. Следуя этим подходам, вы получите тестовую базу, которая не ломается при рефакторинге и даёт уверенность в корректности интерфейса.






Комментарии
0