Тестирование и отладка React: решаем 5 самых частых проблем с RTL, Jest, Enzyme, DevTools и производительностью
Если вы пишете тесты для React-приложений или разбираетесь с тормозами в браузере - эта статья для вас. Здесь собраны конкретные решения пяти проблем, с которыми регулярно сталкиваются разработчики: тест падает с Unable to find an element, моки в Jest не работают, Enzyme ломается на React 18, DevTools не подключается, а приложение рендерится по несколько раз без видимой причины.
Без воды - только рабочие примеры и объяснения, почему это вообще происходит.
1. React Testing Library: тест падает с ошибкой «Unable to find an element»
Что это такое
Unable to find an element by: [data-testid="submit-btn"] или аналогичная ошибка - это сигнал от React Testing Library (RTL), что элемент не найден в DOM в момент обращения к нему.
Почему возникает
Три главные причины:
Асинхронность. Компонент отображает данные после загрузки - вы ищете элемент до того, как он появился.
Неправильный запрос. Ищете getBy, а нужен findBy или waitFor.
Неверный текст или атрибут. RTL чувствителен к регистру и пробелам.
Частые ошибки
// ❌ Неверно: getBy не ждёт, тест сразу падает
test('показывает кнопку', () => {
render(<AsyncComponent />);
const btn = screen.getByTestId('submit-btn'); // падает, если компонент ещё грузится
});Пошаговое решение
Шаг 1. Определите, асинхронный ли компонент. Если данные загружаются через useEffect, fetch, или axios - используйте findBy или waitFor.
// ✅ Верно: findBy возвращает Promise и ждёт появления элемента
test('показывает кнопку после загрузки', async () => {
render(<AsyncComponent />);
const btn = await screen.findByTestId('submit-btn');
expect(btn).toBeInTheDocument();
});Шаг 2. Если элемент появляется не сразу, но findBy всё равно не находит - увеличьте таймаут:
const btn = await screen.findByTestId('submit-btn', {}, { timeout: 3000 });Шаг 3. Замокайте fetch/axios, чтобы тест не ждал реального сетевого запроса:
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Иван' }),
})
);Шаг 4. Если ищете по тексту - убедитесь в точном совпадении. Используйте регулярное выражение, если текст может меняться:
// ✅ Регулярное выражение - нечувствительно к регистру
screen.getByText(/отправить/i);Шаг 5. Добавьте screen.debug() перед проблемной строкой - это выведет текущее состояние DOM и сразу покажет, что именно не так:
screen.debug(); // распечатает HTML в консольПримеры
// Компонент загружает пользователя и показывает имя
function UserProfile() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
if (!user) return <p>Загрузка...</p>;
return <h1 data-testid="username">{user.name}</h1>;
}
// ✅ Правильный тест
test('показывает имя пользователя', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ name: 'Мария' }) })
);
render(<UserProfile />);
const heading = await screen.findByTestId('username');
expect(heading).toHaveTextContent('Мария');
});2. React Jest: mock модулей не работает, функции не заменяются
Что это такое
jest.mock() должен подменять модуль на фиктивную реализацию. Но бывает, что функция всё равно вызывает реальный код, или мок применяется не к тому модулю.
Почему возникает
jest.mock() вызван не в правильном месте (не в начале файла).
Используется ES-модуль с import, а Jest работает в CommonJS-режиме без трансформации.
Мокируется не тот путь: реальный путь и путь в моке отличаются.
Функция-стрелка в модуле не заменяется из-за замыкания.
Частые ошибки
// ❌ Неверно: mock вызван внутри beforeEach - слишком поздно
beforeEach(() => {
jest.mock('./api'); // не сработает
});
// ❌ Неверно: путь к модулю не совпадает
jest.mock('../services/api'); // а импорт идёт из '../../services/api'Пошаговое решение
Шаг 1. Всегда вызывайте jest.mock() на верхнем уровне файла, до любых import/require. Jest поднимает (hoist) эти вызовы, но только с верхнего уровня.
// ✅ Верно: mock в начале файла
jest.mock('./api');
import { fetchUser } from './api';Шаг 2. Проверьте, что путь в jest.mock() совпадает точь-в-точь с тем, как он указан в тестируемом модуле - не с точки зрения тестового файла.
Шаг 3. Если нужно заменить только одну функцию в модуле:
jest.mock('./api', () => ({
...jest.requireActual('./api'), // оставляем реальные функции
fetchUser: jest.fn().mockResolvedValue({ name: 'Тест' }),
}));Шаг 4. Для ES-модулей с export default мокайте так:
jest.mock('./logger', () => ({
__esModule: true,
default: jest.fn(),
}));Шаг 5. Сбрасывайте моки между тестами, чтобы состояние не текло из одного теста в другой:
afterEach(() => {
jest.clearAllMocks();
});Примеры
// Модуль api.js
export const getProduct = async (id) => {
const res = await fetch(`/api/products/${id}`);
return res.json();
};
// ✅ Правильный мок в тесте
jest.mock('./api', () => ({
getProduct: jest.fn(),
}));
import { getProduct } from './api';
test('отображает название продукта', async () => {
getProduct.mockResolvedValueOnce({ id: 1, title: 'Кофемашина' });
render(<ProductCard id={1} />);
expect(await screen.findByText('Кофемашина')).toBeInTheDocument();
});3. React Enzyme: shallow render не работает в React 18
Что это такое
Enzyme - популярная библиотека для тестирования React-компонентов. shallow рендерит компонент без дочерних и хорошо подходил для изолированного тестирования. В React 18 официальная поддержка Enzyme отсутствует.
Почему возникает
Enzyme 3.x использует внутренние API React, которые изменились в React 16.9 и были убраны в React 17–18. Официальный адаптер enzyme-adapter-react-18 на момент написания статьи не поставляется от основной команды Enzyme.
Частые ошибки
Error: Enzyme Internal Error: Enzyme expects an adapter to be configured,
but found none.TypeError: Cannot read properties of undefined (reading 'ReactDOM')Пошаговое решение
Вариант А: использовать сторонний адаптер
Существует неофициальный адаптер @wojtekmaj/enzyme-adapter-react-18:
npm install --save-dev @wojtekmaj/enzyme-adapter-react-18// setupTests.js
import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-18';
Enzyme.configure({ adapter: new Adapter() });Адаптер поддерживает большинство базовых сценариев, но некоторые возможности Enzyme могут работать нестабильно.
Вариант Б (рекомендуемый): мигрировать на React Testing Library
RTL - официально рекомендуемый инструмент для тестирования React 18. Он проверяет поведение с точки зрения пользователя, а не детали реализации.
// Enzyme (устаревший подход)
const wrapper = shallow(<Button label="Купить" />);
expect(wrapper.find('button').text()).toBe('Купить');
// RTL (современный подход)
render(<Button label="Купить" />);
expect(screen.getByRole('button', { name: 'Купить' })).toBeInTheDocument();Шаг 1. Установите RTL, если ещё не сделали:
npm install --save-dev @testing-library/react @testing-library/jest-domШаг 2. Добавьте в jest.config.js или package.json:
{
"jest": {
"setupFilesAfterFramework": ["@testing-library/jest-dom"]
}
}Шаг 3. Перепишите тесты, фокусируясь на том, что видит и делает пользователь - не на внутренней структуре компонента.
4. React DevTools: не подключается к приложению, расширения нет в браузере
Что это такое
React DevTools - расширение для Chrome/Firefox, которое позволяет инспектировать дерево компонентов, смотреть props и state, профилировать рендеры.
Почему возникает
Расширение не установлено или отключено в браузере.
Приложение собрано в production-режиме - DevTools не подключаются к минифицированной сборке.
Используется iframe или расширение не имеет доступа к странице (например, chrome-extension:// страница).
Несовместимость версий: очень старая версия DevTools против React 18+.
Пошаговое решение
Шаг 1. Установите расширение:
Chrome Web Store
Firefox Add-ons
Шаг 2. Убедитесь, что приложение запущено в development-режиме:
# Create React App
npm start # запускает в dev-режиме
# Vite
npm run devВ production-сборке (npm run build) React DevTools не работают - это намеренное ограничение.
Шаг 3. Перезагрузите страницу после установки расширения - DevTools подхватываются при загрузке страницы, а не динамически.
Шаг 4. Если используете локальный сервер на localhost - проверьте, что расширение имеет разрешение на доступ к локальным URL. В Chrome: Extensions → React Developer Tools → «Allow access to file URLs».
Шаг 5. Автономный вариант (без браузерного расширения) - полезен в Electron, React Native или нестандартных окружениях:
npm install -g react-devtools
react-devtoolsВ приложении добавьте скрипт до загрузки React:
<script src="http://localhost:8097"></script>Шаг 6. Обновите расширение - старые версии (до 4.x) не поддерживают React 17+.
5. React performance: приложение тормозит, много лишних рендеров
Что это такое
Лишние рендеры - ситуация, когда компонент перерисовывается без реальных изменений в данных. Это замедляет приложение и плохо сказывается на UX, особенно при работе со списками или сложными формами.
Почему возникает
Родительский компонент рендерится, и все дочерние - тоже, даже если их props не изменились.
Создание новых объектов/функций при каждом рендере нарушает сравнение по ссылке.
Context вызывает ре-рендер у всех потребителей, даже если изменилась только часть значения.
Отсутствует мемоизация там, где она нужна.
Как найти проблему
Откройте React DevTools → вкладка Profiler → нажмите «Record», взаимодействуйте с приложением, остановите запись. DevTools покажет, какие компоненты рендерились и почему.
Также включите «Highlight updates» в настройках DevTools - компоненты при рендере будут подсвечиваться рамкой.
Пошаговое решение
Шаг 1. React.memo для функциональных компонентов
// ❌ Рендерится каждый раз, когда родитель обновляется
function UserCard({ name, age }) {
return <div>{name}, {age} лет</div>;
}
// ✅ Рендерится только при реальном изменении props
const UserCard = React.memo(function UserCard({ name, age }) {
return <div>{name}, {age} лет</div>;
});Шаг 2. useCallback для функций, передаваемых в дочерние компоненты
// ❌ Каждый рендер создаёт новую ссылку на функцию
function Parent() {
const handleClick = () => console.log('клик');
return <Child onClick={handleClick} />;
}
// ✅ Ссылка стабильна между рендерами
function Parent() {
const handleClick = useCallback(() => console.log('клик'), []);
return <Child onClick={handleClick} />;
}Шаг 3. useMemo для тяжёлых вычислений
// ✅ Вычисляется только при изменении items
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.price - b.price),
[items]
);Шаг 4. Разбивайте Context
Если в одном контексте хранятся и часто меняющиеся данные, и редко меняющиеся - разделите их:
// ❌ Одно хранилище для всего
const AppContext = createContext({ user, theme, notifications });
// ✅ Отдельные контексты
const UserContext = createContext(user);
const ThemeContext = createContext(theme);Шаг 5. Виртуализация длинных списков
Для списков из сотен и тысяч элементов используйте react-window или react-virtual:
npm install react-windowimport { FixedSizeList } from 'react-window';
function Row({ index, style }) {
return <div style={style}>Элемент {index}</div>;
}
<FixedSizeList height={400} itemCount={1000} itemSize={35} width="100%">
{Row}
</FixedSizeList>Часто задаваемые вопросы (FAQ)
1. Чем findBy отличается от getBy в React Testing Library?
getBy - синхронный, бросает ошибку сразу, если элемент не найден. findBy - асинхронный, возвращает Promise и ждёт появления элемента (по умолчанию до 1 000 мс). Используйте findBy для асинхронных компонентов.
2. Можно ли использовать Enzyme в React 18?
Можно с неофициальным адаптером @wojtekmaj/enzyme-adapter-react-18, но это не рекомендуется для новых проектов. Лучше перейти на React Testing Library.
3. Почему jest.mock не работает внутри beforeEach?
Jest поднимает (hoist) вызовы jest.mock() в начало файла только если они находятся на верхнем уровне. Вызов внутри функции не поднимается и не успевает перехватить импорт.
4. React DevTools показывает пустое дерево - почему?
Скорее всего, приложение собрано в production-режиме. DevTools работают только с development-сборкой. Проверьте, что вы запустили npm start или npm run dev, а не npm run build.
5. React.memo не останавливает ре-рендер - что не так?
Скорее всего, props-объект или функция создаётся заново при каждом рендере родителя. React.memo использует поверхностное сравнение - новая ссылка на объект считается изменением. Добавьте useCallback для функций и useMemo для объектов.
6. Как узнать, почему компонент рендерится?
Установите why-did-you-render:
npm install --save-dev @welldone-software/why-did-you-render// wdyr.js (подключите в начало index.js)
import React from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });7. Нужен ли useCallback всегда?
Нет. useCallback полезен только когда функция передаётся в мемоизированный дочерний компонент или в зависимости useEffect/useMemo. В остальных случаях - лишние затраты.
8. Как протестировать компонент, использующий Context?
Оберните компонент в Provider прямо в тесте:
render(
<ThemeContext.Provider value="dark">
<MyComponent />
</ThemeContext.Provider>
);Полезные советы и лучшие практики
Приоритет запросов в RTL: предпочитайте getByRole → getByLabelText → getByText → getByTestId. data-testid - последний вариант, когда другие не подходят.
Не тестируйте детали реализации: тесты должны проверять поведение с точки зрения пользователя, а не внутренние переменные или методы.
Изолируйте тесты: каждый тест должен быть независимым. Используйте beforeEach для установки начального состояния и afterEach для очистки.
Не злоупотребляйте мемоизацией: React.memo, useMemo, useCallback добавляют затраты сами по себе. Применяйте их только после профилирования, когда проблема реально есть.
Профилируйте перед оптимизацией: React DevTools Profiler покажет, где теряется время. Не оптимизируйте вслепую.
Обновите DevTools: версия расширения должна соответствовать версии React. Для React 18 нужна DevTools 4.x+.
Используйте screen вместо wrapper: в RTL screen всегда предпочтительнее деструктурирования из render.
Итог
Пять проблем - пять конкретных решений:
Unable to find an element → используйте findBy для асинхронных компонентов и screen.debug() для диагностики.
Mock не работает → jest.mock() должен быть на верхнем уровне файла, путь - точно как в тестируемом модуле.
Enzyme + React 18 → неофициальный адаптер или (лучше) миграция на RTL.
DevTools не подключается → development-режим, перезагрузка страницы, разрешение на localhost.
Лишние рендеры → React.memo, useCallback, useMemo, разделение контекста, виртуализация списков.
Тестирование и отладка React - навык, который сильно ускоряет разработку. Потратив час на правильную настройку инструментов, вы экономите дни на поиске багов.
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.