Мокирование таймеров

Таймеры — важная часть асинхронного программирования в JavaScript. В процессе тестирования часто возникает необходимость замедлить выполнение тестов, подменить поведение таймеров или изменить их взаимодействие с кодом. Jest предоставляет ряд инструментов для эффективного мокирования таймеров, таких как setTimeout, setInterval, а также функции, связанные с их отменой — clearTimeout, clearInterval. Мокирование этих функций позволяет тестировать асинхронный код, не затрачивая время на фактические задержки.

Функции для мокирования таймеров

Jest предлагает встроенные методы для работы с таймерами:

  • jest.useFakeTimers() — переключает Jest в режим использования фальшивых таймеров.
  • jest.runAllTimers() — выполняет все отложенные таймеры, включая все функции, ожидающие выполнения через setTimeout или setInterval.
  • jest.runOnlyPendingTimers() — выполняет только те таймеры, которые находятся в очереди на выполнение, без выполнения тех, которые были запланированы позже.
  • jest.advanceTimersByTime(ms) — продвигает время на указанное количество миллисекунд, что позволяет “прокачать” таймеры вперед.

Включение фальшивых таймеров

Когда используется jest.useFakeTimers(), Jest подменяет стандартные таймеры на свои фальшивые аналоги. Это позволяет вам контролировать, когда и как должны срабатывать таймеры, и использовать такие методы, как runAllTimers для эмуляции времени без ожидания реальных задержек. Включить фальшивые таймеры можно следующим образом:

jest.useFakeTimers();

Этот метод также позволяет мокировать поведение таймеров для тестов с асинхронным кодом, который использует таймеры для задержки выполнения.

Мокирование setTimeout и setInterval

Когда тестируемый код использует функции setTimeout или setInterval, вы можете управлять их выполнением с помощью фальшивых таймеров. Например:

test('mocking setTimeout with fake timers', () => {
  jest.useFakeTimers();

  const callback = jest.fn();
  setTimeout(callback, 1000);

  // Таймер ещё не сработал
  expect(callback).not.toHaveBeenCalled();

  // Прокачиваем время
  jest.advanceTimersByTime(1000);

  // Таймер сработал
  expect(callback).toHaveBeenCalled();
});

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

Поведение с clearTimeout и clearInterval

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

Пример использования clearTimeout в тестах:

test('clearTimeout works with fake timers', () => {
  jest.useFakeTimers();

  const callback = jest.fn();
  const timerId = setTimeout(callback, 1000);
  
  // Останавливаем таймер до того, как он сработает
  clearTimeout(timerId);
  
  // Прокачиваем время
  jest.advanceTimersByTime(1000);
  
  // Таймер не должен был сработать
  expect(callback).not.toHaveBeenCalled();
});

Тестирование асинхронного кода с таймерами

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

Пример:

test('async function with timers', () => {
  jest.useFakeTimers();

  const asyncFunction = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('done');
      }, 1000);
    });
  };

  // Запускаем асинхронную функцию
  const promise = asyncFunction();
  
  // Таймер ещё не сработал, промис ещё не завершён
  expect(promise).not.toBeResolved();

  // Прокачиваем время
  jest.advanceTimersByTime(1000);

  // Промис должен быть завершён
  return expect(promise).resolves.toBe('done');
});

В данном примере асинхронная функция использует setTimeout, и с помощью jest.advanceTimersByTime мы можем контролировать завершение промиса, не дожидаясь реальной задержки.

Очистка после тестов

Когда в тестах используются фальшивые таймеры, важно вернуть всё к нормальному состоянию после их использования. Для этого Jest предоставляет метод jest.useRealTimers(), который восстанавливает оригинальные таймеры.

test('mocking setTimeout and restoring real timers', () => {
  jest.useFakeTimers();

  const callback = jest.fn();
  setTimeout(callback, 1000);
  
  jest.runAllTimers();

  expect(callback).toHaveBeenCalled();
  
  // Восстанавливаем оригинальные таймеры
  jest.useRealTimers();
});

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

Важные особенности

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

Пример использования для асинхронных операций

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

test('async operation with setTimeout', () => {
  jest.useFakeTimers();
  
  const fetchData = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('data');
      }, 2000);
    });
  };

  const dataPromise = fetchData();

  // Прокачиваем время на 2000 миллисекунд
  jest.advanceTimersByTime(2000);

  return expect(dataPromise).resolves.toBe('data');
});

В данном примере мокирование таймеров позволяет нам не ждать фактической задержки в 2 секунды, а сразу проверить результат.

Заключение

Мокирование таймеров в Jest — мощный инструмент для управления временем в тестах, позволяя ускорить их выполнение и избежать зависимости от реальных временных задержек. Это особенно полезно для тестирования асинхронных операций, которые используют setTimeout и setInterval. Умение правильно работать с мокированными таймерами делает тесты более контролируемыми и быстрыми, что способствует повышению качества и скорости разработки.