Асинхронные действия пользователя

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

Основные подходы к асинхронному тестированию

1. Использование waitFor

Функция waitFor применяется для ожидания появления или изменения элементов в DOM. Она периодически проверяет условие до того момента, пока оно не станет истинным или не истечет таймаут.

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

test('кнопка обновляет текст после асинхронного действия', async () => {
  render(<AsyncButton />);

  userEvent.click(screen.getByText('Нажми меня'));

  await waitFor(() => {
    expect(screen.getByText('Загрузка завершена')).toBeInTheDocument();
  });
});

Ключевые моменты:

  • waitFor возвращает промис, поэтому тест должен быть асинхронным (async).
  • Внутри переданной функции проверяется состояние DOM, а не внутренние методы компонента.
  • Таймаут по умолчанию — 1000 мс, его можно изменять через { timeout: 3000 }.

2. Использование findBy

Методы с префиксом findBy (findByText, findByRole и т.д.) объединяют поиск элемента и ожидание его появления. Это сокращает необходимость оборачивать проверку в waitFor.

test('отображает данные после загрузки', async () => {
  render(<AsyncData />);

  const item = await screen.findByText('Данные загружены');
  expect(item).toBeInTheDocument();
});

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

  • findBy возвращает промис, который разрешается, когда элемент найден.
  • Если элемент не появляется в течение таймаута, тест падает с ошибкой.
  • Удобно использовать для простых асинхронных ожиданий.

Асинхронные действия пользователя

userEvent и задержки

Библиотека userEvent эмулирует действия пользователя, включая клики, ввод текста и наведение курсора. В асинхронных сценариях важно учитывать, что некоторые действия могут быть асинхронными. Например, userEvent.type возвращает промис, если используется с опцией задержки между символами:

await userEvent.type(screen.getByRole('textbox'), 'Привет', { delay: 50 });

Ожидание изменений состояния

Когда пользователь инициирует действие, которое запускает асинхронное обновление состояния (например, запрос к API), правильное тестирование заключается в ожидании видимого результата, а не времени выполнения.

test('форма отправки данных отображает сообщение после ответа сервера', async () => {
  render(<SubmitForm />);

  userEvent.click(screen.getByText('Отправить'));

  await screen.findByText('Данные успешно отправлены');
});

Проверка последовательности действий

Для сложных компонентов, где одно действие пользователя запускает цепочку асинхронных изменений, рекомендуется чётко структурировать тест:

  1. Выполнить действие пользователя.
  2. Дождаться обновления DOM через waitFor или findBy.
  3. Проверить промежуточные состояния, если необходимо.
  4. Проверить финальное состояние компонента.
test('многошаговая асинхронная форма', async () => {
  render(<MultiStepForm />);

  userEvent.click(screen.getByText('Начать'));

  // Ждём появления первого шага
  await screen.findByText('Шаг 1');

  userEvent.click(screen.getByText('Далее'));

  await screen.findByText('Шаг 2');
  userEvent.click(screen.getByText('Завершить'));

  await screen.findByText('Форма успешно отправлена');
});

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

Иногда асинхронное действие реализовано через setTimeout или setInterval. RTL совместим с Jest таймерами:

jest.useFakeTimers();

test('отображает уведомление через 2 секунды', () => {
  render(<DelayedNotification />);

  jest.advanceTimersByTime(2000);

  expect(screen.getByText('Уведомление')).toBeInTheDocument();
});
  • Использование fake timers ускоряет тесты и исключает реальные задержки.
  • После использования рекомендуется сбросить таймеры через jest.useRealTimers().

Ошибки и отказы

Асинхронные тесты должны проверять обработку ошибок:

test('отображает сообщение об ошибке при неудачном запросе', async () => {
  render(<AsyncData fail />);

  await screen.findByText('Ошибка загрузки данных');
});

Это гарантирует, что UI корректно реагирует на сбои сервера или сети.

Практические рекомендации

  • Всегда ожидать результат в DOM, а не внутренние вызовы функций.
  • Избегать использования setTimeout в тестах для ожидания состояния — лучше waitFor или findBy.
  • Использовать асинхронные версии userEvent, если действия предполагают задержки.
  • Для сложных последовательностей разбивать тест на шаги с проверкой каждого изменения.
  • Таймауты на ожидание можно настраивать через опции waitFor или findBy.

Асинхронное тестирование в React Testing Library становится безопасным и стабильным, если ориентироваться на пользовательский интерфейс и видимые изменения, а не на внутреннюю реализацию. Такой подход делает тесты более читаемыми, устойчивыми к изменениям и приближенными к реальному поведению пользователей.