Тестирование и отладка React: решаем 5 самых частых проблем с RTL, Jest, Enzyme, DevTools и производительностью

mr. Cooper 1 день назад Веб-разработка
Тестирование и отладка 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-window
import { 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.

Итог

Пять проблем - пять конкретных решений:

  1. Unable to find an element → используйте findBy для асинхронных компонентов и screen.debug() для диагностики.

  2. Mock не работает → jest.mock() должен быть на верхнем уровне файла, путь - точно как в тестируемом модуле.

  3. Enzyme + React 18 → неофициальный адаптер или (лучше) миграция на RTL.

  4. DevTools не подключается → development-режим, перезагрузка страницы, разрешение на localhost.

  5. Лишние рендеры → React.memo, useCallback, useMemo, разделение контекста, виртуализация списков.

Тестирование и отладка React - навык, который сильно ускоряет разработку. Потратив час на правильную настройку инструментов, вы экономите дни на поиске багов.

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи