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

Тестирование асинхронных операций является важной частью любого процесса тестирования, и функции setTimeout и setInterval представляют собой часто используемые инструменты для выполнения отложенных и повторяющихся действий. Однако при написании юнит-тестов для кода, использующего эти функции, необходимо учитывать их асинхронную природу. Jest предоставляет мощные инструменты для эффективного тестирования этих операций, позволяя симулировать и контролировать время, избегая реальных задержек.

1. Основы работы с setTimeout и setInterval

setTimeout и setInterval — это стандартные функции JavaScript, которые позволяют управлять временем выполнения кода:

  • setTimeout — вызывает функцию один раз через заданный промежуток времени.
  • setInterval — вызывает функцию через заданные интервалы времени повторно.

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

2. Проблемы тестирования асинхронных операций

При использовании setTimeout и setInterval тестирование может столкнуться с рядом проблем. В частности:

  • Задержки времени: Функции требуют определённого времени для выполнения, что может замедлить выполнение тестов, особенно когда таких операций несколько.
  • Трудность синхронизации: Реальные таймеры могут вызывать трудности при проверке результатов в нужный момент.

Jest решает эти проблемы, предоставляя средства для управления временем, такие как функции для контроля времени и моки для таймеров.

3. Использование Jest для тестирования setTimeout и setInterval

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

  • jest.useFakeTimers() — включает использование фейковых таймеров.
  • jest.runAllTimers() — выполняет все ожидающие таймеры.

Для начала, необходимо активировать фейковые таймеры с помощью jest.useFakeTimers():

jest.useFakeTimers();

После этого любые вызовы setTimeout и setInterval будут контролироваться Jest, и можно будет манипулировать временем, не затрачивая фактическое время ожидания.

4. Тестирование setTimeout

Для того чтобы протестировать код, использующий setTimeout, необходимо:

  1. Включить фейковые таймеры с помощью jest.useFakeTimers().
  2. Вызвать функцию, содержащую setTimeout.
  3. Симулировать прохождение времени с помощью jest.advanceTimersByTime(ms) или jest.runAllTimers().
  4. Проверить, был ли вызван нужный колбэк.

Пример:

test('setTimeout вызывает колбэк после задержки', () => {
  jest.useFakeTimers();
  const mockCallback = jest.fn();

  setTimeout(mockCallback, 1000);

  // Убедимся, что колбэк не вызван сразу
  expect(mockCallback).not.toHaveBeenCalled();

  // Симулируем прохождение времени
  jest.advanceTimersByTime(1000);

  // Проверяем, что колбэк был вызван
  expect(mockCallback).toHaveBeenCalled();
});

В этом примере:

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

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

Для тестирования функции, использующей setInterval, принцип остаётся схожим. В отличие от setTimeout, здесь важно учитывать, что интервал будет повторяться до тех пор, пока не будет отменён. Для этого можно использовать jest.runAllTimers(), чтобы сразу выполнить все интервальные таймеры.

Пример:

test('setInterval вызывает колбэк повторно через интервал', () => {
  jest.useFakeTimers();
  const mockCallback = jest.fn();

  setInterval(mockCallback, 1000);

  // Симулируем прохождение времени
  jest.advanceTimersByTime(1000);

  // Проверяем, что колбэк был вызван 1 раз
  expect(mockCallback).toHaveBeenCalledTimes(1);

  // Симулируем ещё 1000 мс
  jest.advanceTimersByTime(1000);

  // Проверяем, что колбэк был вызван 2 раза
  expect(mockCallback).toHaveBeenCalledTimes(2);
});

Здесь:

  • Мы вызываем setInterval, который должен вызывать mockCallback каждые 1000 мс.
  • После каждого вызова jest.advanceTimersByTime(1000) проверяем, сколько раз был вызван колбэк.
  • Важно отметить, что в отличие от setTimeout, setInterval будет вызываться многократно, пока не будет отменён.

6. Остановка таймеров

При тестировании важно учитывать возможность отмены таймеров, особенно для setInterval, чтобы предотвратить бесконечные циклы. Для этого можно использовать clearTimeout и clearInterval.

Пример:

test('clearInterval останавливает повторяющийся вызов', () => {
  jest.useFakeTimers();
  const mockCallback = jest.fn();
  const intervalId = setInterval(mockCallback, 1000);

  // Симулируем прохождение времени
  jest.advanceTimersByTime(3000);

  // Проверяем, что колбэк был вызван 3 раза
  expect(mockCallback).toHaveBeenCalledTimes(3);

  // Останавливаем интервал
  clearInterval(intervalId);

  // Симулируем ещё 1000 мс
  jest.advanceTimersByTime(1000);

  // Проверяем, что колбэк больше не был вызван
  expect(mockCallback).toHaveBeenCalledTimes(3);
});

Здесь:

  • После того как таймер отработал 3 раза, мы вызываем clearInterval, чтобы остановить дальнейшие вызовы.
  • Проверяем, что колбэк не был вызван после остановки интервала.

7. Использование jest.runAllTimers()

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

Пример:

test('jest.runAllTimers выполняет все таймеры', () => {
  jest.useFakeTimers();
  const mockCallback = jest.fn();

  setTimeout(mockCallback, 1000);
  setTimeout(mockCallback, 2000);

  // Проверяем, что колбэк ещё не был вызван
  expect(mockCallback).not.toHaveBeenCalled();

  // Выполняем все таймеры
  jest.runAllTimers();

  // Проверяем, что колбэк был вызван 2 раза
  expect(mockCallback).toHaveBeenCalledTimes(2);
});

В этом примере:

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

8. Заключение

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