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

Введение
Тестирование React-компонентов — это не просто модная практика, а способ сэкономить часы на ручной проверке UI после каждого изменения. Связка Jest и React Testing Library (RTL) стала стандартом в экосистеме React: Jest берёт на себя запуск тестов, моки и ассерты, а RTL предлагает API, которое заставляет писать тесты так, как реальный пользователь взаимодействует с интерфейсом — через клики, ввод текста и поиск элементов по доступным ролям, а не по внутренним деталям реализации.
В этой статье разберём, как настроить тестовое окружение, написать первые тесты компонентов, протестировать пользовательские события, асинхронные запросы, мокировать зависимости и проверить кастомные хуки.
Установка и настройка
Если проект создан через Create React App, Jest и RTL уже встроены. Для Vite или кастомной сборки на Webpack нужно установить зависимости вручную:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom
Базовый конфиг jest.config.js:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // имитация браузерного окружения
setupFilesAfterEach: ['@testing-library/jest-dom'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy', // игнорируем стили
},
};
Файл настройки для подключения дополнительных матчеров:
// jest.setup.js
import '@testing-library/jest-dom';
Первый тест компонента
Допустим, есть простой компонент счётчика:
// Counter.jsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">Счёт: {count}</p>
<button onClick={() => setCount(count + 1)}>Увеличить</button>
</div>
);
}
Тест для него:
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import { Counter } from './Counter';
test('отображает начальное значение счётчика', () => {
render(<Counter />);
// ищем элемент по тексту, как это делал бы пользователь
expect(screen.getByText('Счёт: 0')).toBeInTheDocument();
});
Главный принцип RTL — искать элементы так, как их видит пользователь: по тексту, роли или подписи, а не по CSS-классам.
Работа с пользовательскими событиями
Для имитации кликов и ввода текста используется библиотека user-event, которая точнее воспроизводит реальное поведение браузера, чем встроенный fireEvent:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
test('увеличивает счётчик при клике', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: 'Увеличить' });
await user.click(button);
expect(screen.getByText('Счёт: 1')).toBeInTheDocument();
});
Тестирование асинхронного кода
Компоненты часто загружают данные после монтирования. Для ожидания появления элемента после асинхронной операции используются асинхронные варианты запросов:
// UserProfile.jsx
import { useEffect, useState } from 'react';
export function UserProfile({ userId }) {
const [name, setName] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setName(data.name));
}, [userId]);
if (!name) return <p>Загрузка...</p>;
return <p>Имя: {name}</p>;
}
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
test('показывает имя после загрузки', async () => {
// мокируем глобальный fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Анна' }),
})
);
render(<UserProfile userId={1} />);
// findBy автоматически ждёт появления элемента
const nameElement = await screen.findByText('Имя: Анна');
expect(nameElement).toBeInTheDocument();
});
Мокирование зависимостей
Jest позволяет подменять модули целиком, что удобно для изоляции компонента от внешних сервисов:
// api.js
export async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
import { fetchUser } from './api';
jest.mock('./api');
test('отображает имя пользователя из мока', async () => {
fetchUser.mockResolvedValue({ name: 'Иван' });
render(<UserProfile userId={2} />);
expect(await screen.findByText('Имя: Иван')).toBeInTheDocument();
});
Тестирование кастомных хуков
Для изолированного тестирования хуков используется renderHook из @testing-library/react:
// useCounter.js
import { useState, useCallback } from 'react';
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
return { count, increment };
}
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('увеличивает значение при вызове increment', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6);
});
Частые ошибки
Поиск элементов по CSS-классам или id вместо роли и текста. Такой тест ломается при любом рефакторинге разметки, хотя поведение компонента не изменилось.
Использование fireEvent там, где нужен userEvent. fireEvent не вызывает побочные эффекты браузера, например фокус, из-за чего тесты пропускают реальные баги.
Забытый await перед асинхронными действиями user-event и findBy-запросами. Без await тест может завершиться раньше, чем обновится DOM, и дать ложноположительный результат.
Чрезмерное мокирование. Если замокать слишком много зависимостей, тест начинает проверять не поведение компонента, а корректность самих моков.
Тестирование деталей реализации вместо результата. Проверка внутреннего состояния через instance-методы или приватные переменные делает тесты хрупкими и бесполезными при рефакторинге.
Отсутствие очистки между тестами. Если не сбрасывать моки и таймеры после каждого теста, результаты одного теста могут влиять на следующий.
Заключение
Jest и React Testing Library вместе образуют связку, которая заставляет писать тесты, ориентированные на поведение пользователя, а не на внутреннюю структуру компонентов. Такой подход делает тесты устойчивыми к рефакторингу и действительно полезными: они ловят регрессии в логике, а не в деталях реализации. Начать стоит с простых тестов рендера и событий, затем постепенно добавлять проверку асинхронных операций, моков и кастомных хуков — это покроет большинство реальных сценариев в React-приложениях.



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