Тестирование useEffect хуков

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


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

  1. Эффекты, вызывающие рендеринг изменений состояния

Часто useEffect используется для обновления состояния после монтирования компонента или изменения пропсов:

useEffect(() => {
  setData(fetchData());
}, [dependency]);

В тестах необходимо убедиться, что состояние компонента меняется корректно, что можно проверить через визуальные изменения или DOM-узлы, а не через прямое обращение к состоянию.

Пример:

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

jest.mock('./api');

test('отрисовывает данные после эффекта', async () => {
  fetchData.mockResolvedValue(['Item 1', 'Item 2']);
  
  render(<MyComponent />);
  
  await waitFor(() => {
    expect(screen.getByText('Item 1')).toBeInTheDocument();
    expect(screen.getByText('Item 2')).toBeInTheDocument();
  });
});

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

  • Использование waitFor для ожидания асинхронного эффекта.
  • Проверка результата в DOM, а не внутреннего состояния компонента.
  • Мокирование внешних вызовов для изоляции теста.

  1. Эффекты с побочными действиями (HTTP-запросы, подписки, таймеры)

Эффекты могут инициировать сетевые запросы или взаимодействовать с другими сервисами. Тестирование таких эффектов требует изоляции и контроля внешних зависимостей.

Подходы:

  • Мокирование API: Использование jest.mock или специализированных библиотек вроде msw.
  • Контроль таймеров: Для таймеров внутри useEffect применяются методы jest.useFakeTimers и jest.runAllTimers.

Пример таймера:

useEffect(() => {
  const interval = setInterval(() => {
    setCount(prev => prev + 1);
  }, 1000);
  return () => clearInterval(interval);
}, []);

Тест:

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

jest.useFakeTimers();

test('увеличивает счетчик каждые 1000 мс', () => {
  render(<Counter />);
  
  expect(screen.getByText('0')).toBeInTheDocument();
  
  jest.advanceTimersByTime(1000);
  expect(screen.getByText('1')).toBeInTheDocument();
  
  jest.advanceTimersByTime(2000);
  expect(screen.getByText('3')).toBeInTheDocument();
});

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

  • Использование фиктивных таймеров для синхронного тестирования асинхронного кода.
  • Очистка интервалов через return в useEffect проверяется косвенно: отсутствие ошибок при размонтировании.

  1. Очистка эффектов (cleanup)

Многие эффекты возвращают функцию очистки. Важно убедиться, что эта функция выполняется корректно при размонтировании компонента.

Пример:

useEffect(() => {
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Тест:

import { render, unmountComponentAtNode } from '@testing-library/react';
import ResizeComponent from './ResizeComponent';

test('очищает обработчик при размонтировании', () => {
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  render(<ResizeComponent />, { container });
  
  const spy = jest.spyOn(window, 'removeEventListener');
  
  unmountComponentAtNode(container);
  
  expect(spy).toHaveBeenCalledWith('resize', expect.any(Function));
});

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

  • Проверка вызова функции очистки через мок-функции (jest.spyOn).
  • Тестирование побочных эффектов, связанных с DOM, через реальные события или их имитацию.

Асинхронные эффекты и act

React Testing Library автоматически оборачивает большинство действий в act, но при асинхронных эффектах требуется использовать waitFor или findBy методы, чтобы дождаться изменений:

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

Использование await гарантирует, что тест дождется завершения асинхронного эффекта перед проверкой результата.


Тестирование зависимостей useEffect

Проверка правильного срабатывания эффекта при изменении зависимостей:

useEffect(() => {
  console.log('Зависимость изменилась:', propValue);
}, [propValue]);

Тест:

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

test('эффект срабатывает при изменении пропса', () => {
  const { rerender } = render(<PropComponent propValue="A" />);
  
  // Проверка первоначального эффекта
  expect(screen.getByText('A')).toBeInTheDocument();
  
  rerender(<PropComponent propValue="B" />);
  
  // Эффект должен сработать снова
  expect(screen.getByText('B')).toBeInTheDocument();
});

Советы по эффективному тестированию useEffect

  • Проверять результат эффекта, а не внутренние вызовы setState.
  • Использовать моки для внешних зависимостей (fetch, axios, таймеры).
  • Для асинхронных эффектов применять waitFor или findBy* методы.
  • Проверять очистку эффектов через unmount и jest.spyOn.
  • Избегать тестирования реализации; ориентироваться на поведение и визуальные изменения в DOM.

Тестирование useEffect становится надежным, если комбинировать наблюдение за DOM, мокирование внешних зависимостей и корректное использование асинхронных утилит библиотеки.