Обработка Promise и асинхронных операций

В тестировании React-компонентов часто возникает необходимость проверять поведение, связанное с асинхронными операциями: сетевыми запросами, таймерами или динамическим обновлением состояния. React Testing Library предоставляет инструменты для корректного взаимодействия с асинхронным кодом и гарантирует, что тесты будут стабильными и предсказуемыми.


Асинхронные действия в React

Асинхронные операции в компонентах React обычно реализуются через:

  • fetch или axios для получения данных с сервера;
  • таймеры (setTimeout, setInterval);
  • события, вызывающие обновление состояния после задержки.

При тестировании важно дождаться завершения асинхронного действия, чтобы проверка отражала реальное состояние компонента. Простое использование expect сразу после рендера часто приводит к ошибкам, так как DOM ещё не обновился.


Методы для работы с асинхронностью

1. waitFor

waitFor — универсальный инструмент для ожидания изменения DOM. Он повторно выполняет переданную функцию до тех пор, пока не выполнится условие или не истечёт тайм-аут.

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';

test('отображает данные после загрузки', async () => {
  render(<MyComponent />);
  
  userEvent.click(screen.getByText('Загрузить данные'));
  
  await waitFor(() => expect(screen.getByText('Данные загружены')).toBeInTheDocument());
});

Особенности waitFor:

  • Поддерживает пользовательский тайм-аут и интервал опроса.
  • Позволяет ловить ошибки при ожидании.
  • Предпочтителен для случаев, когда нет конкретного метода ожидания элемента, но его появление влияет на тест.

2. findBy и findAllBy

findBy* и findAllBy* — асинхронные версии стандартных селекторов (getBy* и queryBy*). Они автоматически ждут появления элемента в DOM.

test('загружает элемент асинхронно', async () => {
  render(<MyComponent />);
  
  const element = await screen.findByText('Асинхронный текст');
  expect(element).toBeInTheDocument();
});

Преимущества:

  • Простая и лаконичная запись.
  • Встроенное ожидание по умолчанию (timeout ~1000 мс).
  • Удобно для одиночного элемента, когда его появление является критичным.

Тестирование промисов и асинхронных функций

При прямом вызове асинхронной функции можно использовать async/await:

test('проверяет результат промиса', async () => {
  const fetchData = jest.fn().mockResolvedValue('данные');

  const result = await fetchData();
  expect(result).toBe('данные');
});

Для компонентов с побочными эффектами (useEffect с async вызовом) корректное тестирование включает ожидание DOM-обновления:

import { render, screen } from '@testing-library/react';
import DataLoader from './DataLoader';

test('рендерит данные после fetch', async () => {
  render(<DataLoader />);
  
  const loadedData = await screen.findByText('Загруженные данные');
  expect(loadedData).toBeVisible();
});

Важно помнить, что useEffect не блокирует рендер, поэтому проверка должна использовать асинхронные методы (waitFor или findBy).


Работа с таймерами

При тестировании компонентов с setTimeout или setInterval рекомендуется использовать фейковые таймеры Jest:

jest.useFakeTimers();

test('ожидает таймер', () => {
  const callback = jest.fn();
  
  setTimeout(callback, 1000);
  jest.advanceTimersByTime(1000);
  
  expect(callback).toHaveBeenCalled();
});

Особенности:

  • jest.advanceTimersByTime(ms) ускоряет тестирование асинхронного кода без реального ожидания.
  • После теста нужно сбросить таймеры: jest.useRealTimers().

Управление асинхронными ошибками

При тестировании ошибок асинхронных операций важно оборачивать проверки в try/catch или использовать expect(...).rejects:

test('обрабатывает ошибку fetch', async () => {
  const fetchData = jest.fn().mockRejectedValue(new Error('Ошибка сети'));
  
  await expect(fetchData()).rejects.toThrow('Ошибка сети');
});

Для компонентов с асинхронной ошибкой:

render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Ошибка загрузки')).toBeInTheDocument());

waitFor гарантирует, что тест дождётся появления элемента с сообщением об ошибке, даже если ошибка произойдет с задержкой.


Советы по стабильным асинхронным тестам

  • Использовать findBy для простых случаев появления элемента.
  • Применять waitFor для комплексных проверок и последовательных изменений DOM.
  • Избегать использования setTimeout для ожидания вручную (await new Promise(resolve => setTimeout(...))).
  • Мокировать сетевые запросы и промисы через Jest (jest.mock, jest.fn).
  • Следить за очисткой эффектов и таймеров между тестами (afterEach для jest.clearAllMocks() и jest.useRealTimers()).

Асинхронность в React Testing Library строится вокруг корректного ожидания изменений DOM, промисов и таймеров. Правильное использование waitFor, findBy и моков позволяет создавать стабильные и предсказуемые тесты, минимизируя ложные срабатывания и гонки состояний.