Асинхронные операции в тестах: waitFor, act

В React Testing Library взаимодействие с компонентами часто требует работы с асинхронными процессами: сетевыми запросами, таймерами, эффектами useEffect или любыми операциями, изменяющими состояние компонента спустя некоторое время. Для корректного тестирования таких сценариев используются утилиты waitFor и act. Понимание их работы критично для написания надежных тестов.


Основы асинхронного тестирования

Асинхронные операции создают проблему гонок данных: тест может завершиться раньше, чем компонент успеет обновиться. Простая проверка на наличие элемента сразу после рендеринга в таких случаях часто проваливается. Для решения этой проблемы применяется ожидание, позволяющее тесту «дождаться» нужного состояния компонента.

React Testing Library предоставляет два ключевых механизма:

  1. waitFor — ожидает, пока заданное условие станет истинным.
  2. act — оборачивает код, изменяющий состояние компонента, чтобы React корректно применял все обновления перед проверкой результатов.

waitFor: ожидание изменения состояния

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

Синтаксис:

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

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

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

  • Аргумент timeout задаёт максимальное время ожидания. По умолчанию 1000 мс.
  • Если условие не выполнено за указанное время, тест падает с ошибкой.
  • Используется для проверки результатов асинхронных операций, например, рендеринга после запроса к API или изменения состояния после setTimeout.

Пример с fetch-запросом:

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

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([{ id: 1, name: 'Alice' }]),
  })
);

test('отображает пользователей после загрузки', async () => {
  render(<UserList />);
  
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
});

Здесь waitFor гарантирует, что проверка выполняется только после того, как компонент получит данные и обновит DOM.


act: синхронизация с React

Функция act используется для обёртывания любых действий, которые вызывают обновление состояния компонента, включая асинхронные изменения. Это обеспечивает корректное обновление DOM и предотвращает предупреждения о «необёрнутых действиях» (act(...)).

Пример с синхронным обновлением состояния:

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

test('увеличивает счетчик', () => {
  render(<Counter />);
  
  act(() => {
    fireEvent.click(screen.getByText('Увеличить'));
  });
  
  expect(screen.getByText('1')).toBeInTheDocument();
});

Пример с асинхронным обновлением состояния:

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

test('увеличивает счетчик через 500ms', async () => {
  render(<AsyncCounter />);
  
  await act(async () => {
    screen.getByText('Увеличить').click();
    await new Promise(resolve => setTimeout(resolve, 500));
  });
  
  expect(screen.getByText('1')).toBeInTheDocument();
});

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

  • Все асинхронные операции внутри act должны завершаться до выхода из него.
  • Для промисов используется await act(async () => {...}).
  • React гарантирует, что все эффекты и обновления состояния, вызванные внутри act, будут завершены до проверки DOM.

Отличия и сочетание waitFor и act

  • actобёртывает обновление состояния, уведомляя React о необходимости завершить все эффекты перед проверкой.
  • waitForповторяет ассерции, ожидая, пока DOM не примет нужное состояние.

Обычно асинхронные тесты React выглядят так:

  1. Выполняется действие, которое запускает изменение состояния или эффект (fireEvent, вызов функции, рендер).
  2. Обновление состояния оборачивается в act (для сложных случаев, особенно с таймерами).
  3. Проверка результата через waitFor, если DOM меняется асинхронно.

Пример комбинированного использования:

import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import AsyncToggle from './AsyncToggle';

test('переключает состояние асинхронно', async () => {
  render(<AsyncToggle />);
  
  await act(async () => {
    fireEvent.click(screen.getByText('Переключить'));
  });
  
  await waitFor(() => {
    expect(screen.getByText('Включено')).toBeInTheDocument();
  });
});

Такой подход исключает состояния гонки и предупреждения React о «необработанных» обновлениях.


Советы по применению

  1. Не использовать setTimeout для ожидания изменений без waitFor — это ненадёжно и делает тесты медленными.
  2. В асинхронных тестах лучше оборачивать любые действия, которые меняют состояние, в act, даже если кажется, что это не нужно.
  3. waitFor можно использовать с любыми селекторами DOM: getByText, queryByRole, findBy.... Последние (findBy) уже включают внутреннее ожидание.
  4. Для чистых тестов избегать смешивания нескольких таймеров и асинхронных эффектов в одном act — проще делить на логические блоки.

Примечания по производительности

  • Увеличение таймаута waitFor повышает стабильность тестов при медленных асинхронных операциях, но замедляет тестовую прогонку.
  • Частое использование act для мелких синхронных действий не критично, но для асинхронных операций помогает избежать непредсказуемого поведения и предупреждений React.

Асинхронное тестирование в React требует внимательного подхода к состояниям и эффектам. Правильное использование waitFor и act обеспечивает корректные, предсказуемые и стабильные тесты, позволяя безопасно проверять как синхронные, так и асинхронные изменения компонентов.