Оптимизация тестов в React Testing Library: как выжать +43% скорости

#react#testing#performance

Первый раз, когда мои RTL-тесты тормозили настолько, что я начал замечать лаг между fireEvent и ассертами, я списал это на «ну так и должно быть». Потом увидел, как 200-мс тесты превращаются в 3000-мс снежные комья в CI — и полез разбираться.

Откуда берётся тормознутость

Основной косяк RTL — его доброта. Под капотом происходит чудовищное количество работы:

  1. Рендер в JSDOM — хоть и легковесный, но каждый раз полный
  2. Дефолтные оберткиReact.StrictMode, контекстные провайдеры
  3. Избыточные проверки — например, cleanup после каждого теста, даже если DOM не менялся

Но главный пожиратель времени — waitFor. Каждый вызов добавляет 50-100 мс ожидания «на всякий случай», что убивает скорость в сценариях типа:

await waitFor(() => {
  expect(screen.getByText('Done')).toBeInTheDocument();
});
// Даже если элемент появился через 1 мс, 
// RTL будет ждать полный дефолтный таймаут

Практические оптимизации

1. Кастомизация рендера

Вместо стандартного render из RTL заводим свою фабрику с отключенным ненужным:

const fastRender = (ui, options) => {
  return render(ui, {
    ...options,
    wrapper: ({ children }) => (
      <ThemeProvider>{children}</ThemeProvider> // Только нужные провайдеры
    ),
  });
};

// В тестах используем только fastRender
test('should render', () => {
  const { container } = fastRender(<Component />);
});

На практике это даёт +15-20% скорости сразу.

2. Агрессивные моки

RTL дружит с jest.mock, но мы можем пойти дальше:

jest.mock('heavy-module', () => ({
  HeavyComponent: () => <div />, // Пустышка вместо реального компонента
}));

beforeAll(() => {
  // Мокаем тяжелые API раз и навсегда
  global.fetch = jest.fn(() => Promise.resolve({ json: () => ({}) }));
});

Важно: такие моки могут скрыть реальные баги, поэтому используем их только для:

3. Управление таймаутами

waitFor по умолчанию ждёт 1000 мс, но в 90% случаев нам хватит 100-200 мс:

const fastWaitFor = async (assertion) => {
  await waitFor(assertion, { timeout: 200 });
};

test('should load data', async () => {
  await fastWaitFor(() => {
    expect(screen.getByText('Loaded')).toBeInTheDocument();
  });
});

Дополнительно можно выставить в setupTests.js:

configure({
  asyncUtilTimeout: 200, // Глобальный таймаут для всех async-утилит
});

Что не стоит оптимизировать

  1. Переход на shallow rendering — потеряем проверку реального взаимодействия
  2. Полный отказ от waitFor — получим хрупкие тесты
  3. Ручные act везде — RTL уже делает это под капотом

После всех оптимизаций мои тесты ускорились с ~12 до ~7 секунд на сюите из 50 тестов. Главный урок: RTL быстр, но требует настройки под конкретный кейс. Если ваши тесты тормозят — не спешите переписывать их на другую библиотеку, возможно, нужно просто убрать лишнее.


Источник: https://sigh.dev/posts/making-react-testing-library-faster/