Работа с таймаутами и setInterval в тестах

Тестирование компонентов, которые используют таймеры, асинхронные задержки или периодические обновления через setInterval, требует особого подхода. В противном случае тесты могут становиться ненадёжными, долго выполняться или вовсе зависать. React Testing Library (RTL) в связке с Jest предоставляет инструменты для контроля времени, позволяя эмулировать поведение таймеров и ускорять выполнение тестов.


Моделирование таймеров с помощью Jest

Jest позволяет переключиться на использование фиктивных таймеров через jest.useFakeTimers(). Это даёт возможность управлять временем в тесте вручную:

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

jest.useFakeTimers();

test('отображает счетчик через 1 секунду', () => {
  render(<TimerComponent />);
  
  // Изначально счетчик равен 0
  expect(screen.getByText('0')).toBeInTheDocument();

  // Продвигаем таймер на 1 секунду
  jest.advanceTimersByTime(1000);

  expect(screen.getByText('1')).toBeInTheDocument();
});

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

  • jest.useFakeTimers() заменяет реальные таймеры Jest-емуляцией.
  • jest.advanceTimersByTime(ms) позволяет пропустить определённое количество миллисекунд, не ожидая реального времени.
  • После теста стоит вернуть настоящие таймеры через jest.useRealTimers(), чтобы избежать побочных эффектов в других тестах.

Тестирование setInterval

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

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

jest.useFakeTimers();

test('счетчик увеличивается каждые 500ms', () => {
  render(<IntervalCounter />);
  
  expect(screen.getByText('0')).toBeInTheDocument();
  
  // Пропускаем 500ms, счетчик должен увеличить значение
  jest.advanceTimersByTime(500);
  expect(screen.getByText('1')).toBeInTheDocument();
  
  // Ещё 500ms — счетчик увеличивается ещё раз
  jest.advanceTimersByTime(500);
  expect(screen.getByText('2')).toBeInTheDocument();
});

Советы при работе с setInterval:

  • jest.runOnlyPendingTimers() — выполняет все запланированные таймеры без продвижения времени. Полезно, когда необходимо мгновенно выполнить все текущие отложенные функции.
  • jest.clearAllTimers() — очищает все таймеры, что особенно важно для компонентов с повторяющимися интервалами, чтобы избежать утечек памяти между тестами.

Асинхронные обновления и act()

Когда компоненты обновляются через таймеры или интервалы, важно использовать act(), чтобы React корректно обработал изменения состояния:

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

jest.useFakeTimers();

test('правильное обновление состояния с интервалом', () => {
  render(<IntervalCounter />);
  
  act(() => {
    jest.advanceTimersByTime(1500); // три тика по 500ms
  });
  
  expect(screen.getByText('3')).toBeInTheDocument();
});
  • act() гарантирует, что все побочные эффекты и рендеры завершены до проверки элементов.
  • При работе с асинхронным кодом внутри таймеров или промисов всегда нужно оборачивать вызовы таймеров в act().

Комбинирование таймеров с асинхронными событиями

Компоненты часто используют комбинацию setTimeout и сетевых запросов:

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

jest.useFakeTimers();

test('отображает сообщение после задержки', async () => {
  render(<AsyncMessage />);
  
  jest.advanceTimersByTime(2000); // задержка 2 секунды

  await waitFor(() => expect(screen.getByText('Готово')).toBeInTheDocument());
});

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

  • waitFor позволяет ждать изменений в DOM без фиксированной задержки.
  • Продвижение таймеров ускоряет выполнение тестов, не заставляя ждать реальное время.
  • Это предотвращает нестабильность тестов из-за реальных задержек.

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

  1. Всегда очищать таймеры после теста:

    afterEach(() => {
      jest.clearAllTimers();
      jest.useRealTimers();
    });
  2. Не использовать реальные setTimeout и setInterval в тестах, если они влияют на критическую логику. Фейковые таймеры делают тесты быстрыми и предсказуемыми.

  3. Комбинировать act() и jest.advanceTimersByTime для корректного обновления состояния компонентов.

  4. Избегать длинных интервалов в компонентах в тестах. Лучше использовать короткие значения или эмулировать поведение таймера.

  5. Проверять все побочные эффекты таймеров: обновление состояния, вызовы колбэков, изменения DOM.


Обработка циклических эффектов

Если компонент использует setInterval внутри useEffect, важно корректно его очищать:

useEffect(() => {
  const interval = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(interval); // предотвращение утечек
}, []);

Без очистки таймеров jest.clearAllTimers() может не справиться с оставшимися активными интервалами, вызывая неожиданные ошибки или предупреждения React.


Сложные сценарии с множественными таймерами

Для компонентов с несколькими таймерами можно управлять ими отдельно:

jest.useFakeTimers();

const timer1 = jest.fn();
const timer2 = jest.fn();

setTimeout(timer1, 1000);
setInterval(timer2, 500);

jest.advanceTimersByTime(1000); // вызывается timer1 и 2 раза timer2
expect(timer1).toHaveBeenCalledTimes(1);
expect(timer2).toHaveBeenCalledTimes(2);

Такой подход позволяет проверять точное количество вызовов каждого таймера, что важно для компонентов с параллельными асинхронными эффектами.


Контроль таймеров с помощью Jest в React Testing Library делает тесты быстрыми, детерминированными и безопасными, позволяя полностью эмулировать поведение setTimeout и setInterval без необходимости ожидать реальное время.