act функция: обёртывание состояния обновлений

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

Основная концепция act

act — это функция, предоставляемая как React, так и React Testing Library, которая гарантирует, что все обновления состояния компонента обернуты и завершены до выполнения проверок. Это критично для предотвращения предупреждений вида:

Warning: An update to ComponentName inside a test was not wrapped in act(...).

Без использования act тест может либо пройти некорректно, либо стать нестабильным, так как обновления состояния в React происходят асинхронно.

Синхронные обновления состояния

При синхронных изменениях состояния act можно использовать следующим образом:

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

test('увеличивает счетчик при клике', () => {
  const { getByText } = render(<Counter />);
  
  const button = getByText('Увеличить');

  act(() => {
    button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  });

  expect(getByText('Счетчик: 1')).toBeInTheDocument();
});

Здесь act оборачивает клик по кнопке, чтобы обновление состояния счетчика завершилось до проверки DOM.

Асинхронные обновления состояния

Если компонент использует асинхронные операции, например fetch или setTimeout, необходимо применять асинхронную версию act:

import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import AsyncComponent from './AsyncComponent';

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

  expect(screen.getByText('Данные загружены')).toBeInTheDocument();
});

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

Комбинация с пользовательскими событиями

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

import { fireEvent, render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import AsyncButton from './AsyncButton';

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

  const button = screen.getByText('Загрузить данные');

  await act(async () => {
    fireEvent.click(button);
  });

  expect(screen.getByText('Данные загружены')).toBeInTheDocument();
});

Здесь fireEvent.click помещается внутрь act, чтобы все связанные асинхронные состояния успели обновиться до проверки DOM.

Когда act обязателен

  • Любое изменение состояния компонента, которое вызывает повторный рендер, должно быть обернуто в act.
  • Любой эффект useEffect, который изменяет состояние или взаимодействует с DOM.
  • Любая асинхронная логика, включая таймеры, промисы или запросы к API, которые вызывают изменение состояния.

Практические советы

  1. Не оборачивать весь тест в act без необходимости. Оборачивание отдельных действий делает тесты более читаемыми и локализует асинхронность.
  2. Использовать await act(async () => {}) для асинхронных операций, чтобы избежать непредсказуемого поведения теста.
  3. При работе с React Testing Library многие функции (например, fireEvent и userEvent) уже оборачиваются в act, поэтому явное использование необходимо только в сложных сценариях.

Заключение

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

Хотите, я составлю наглядную схему применения act для разных сценариев в React Testing Library?