Clock manipulation для асинхронного кода

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


Подключение фейковых таймеров

Для работы с управляемыми таймерами используется функция jasmine.clock(). Она предоставляет методы для установки и контроля виртуального времени.

beforeEach(() => {
  jasmine.clock().install();
});

afterEach(() => {
  jasmine.clock().uninstall();
});
  • install() активирует фейковые таймеры.
  • uninstall() возвращает стандартное поведение таймеров, предотвращая побочные эффекты в других тестах.

Фейковые таймеры и setTimeout

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

function delayedCallback(callback) {
  setTimeout(() => {
    callback('done');
  }, 1000);
}

it('вызывает callback через 1 секунду', () => {
  const callback = jasmine.createSpy('callback');
  delayedCallback(callback);

  expect(callback).not.toHaveBeenCalled();
  jasmine.clock().tick(1000);
  expect(callback).toHaveBeenCalledWith('done');
});
  • jasmine.createSpy() создаёт шпион для отслеживания вызовов функции.
  • tick(ms) — продвижение виртуального времени на указанное количество миллисекунд.

Работа с setInterval

Аналогично, можно тестировать повторяющиеся таймеры:

let counter = 0;

beforeEach(() => {
  jasmine.clock().install();
  setInterval(() => {
    counter++;
  }, 500);
});

afterEach(() => {
  jasmine.clock().uninstall();
});

it('увеличивает счетчик каждые 500 мс', () => {
  expect(counter).toBe(0);
  jasmine.clock().tick(500);
  expect(counter).toBe(1);
  jasmine.clock().tick(1000);
  expect(counter).toBe(3);
});

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


Замена Date на управляемую

Jasmine позволяет также подменять объект Date, что полезно для тестирования времени и тайм-аутов, зависящих от текущей даты:

beforeEach(() => {
  jasmine.clock().install();
  jasmine.clock().mockDate(new Date(2026, 0, 11));
});

afterEach(() => {
  jasmine.clock().uninstall();
});

it('возвращает фиктивную текущую дату', () => {
  const now = new Date();
  expect(now.getFullYear()).toBe(2026);
  expect(now.getMonth()).toBe(0);
  expect(now.getDate()).toBe(11);
});
  • mockDate() заменяет стандартное поведение Date, делая тесты независимыми от реального времени.

Тестирование асинхронных промисов

Фейковые таймеры могут использоваться с промисами, если они зависят от таймеров. Пример с отложенным промисом:

function delayedPromise() {
  return new Promise(resolve => {
    setTimeout(() => resolve('resolved'), 2000);
  });
}

it('резолвит промис через 2 секунды', async () => {
  const promise = delayedPromise();
  let result;
  promise.then(value => result = value);

  expect(result).toBeUndefined();
  jasmine.clock().tick(2000);
  await promise;
  expect(result).toBe('resolved');
});
  • Промис срабатывает синхронно после вызова tick().
  • await используется для корректного завершения промиса в тесте.

Комбинация с асинхронными колбэками

Часто встречаются ситуации, когда асинхронный код комбинирует setTimeout и промисы:

function complexAsync(callback) {
  setTimeout(() => {
    Promise.resolve().then(() => callback('done'));
  }, 1000);
}

it('вызывает callback после таймера и промиса', async () => {
  const callback = jasmine.createSpy('callback');
  complexAsync(callback);

  expect(callback).not.toHaveBeenCalled();
  jasmine.clock().tick(1000);
  await Promise.resolve(); // даём промису выполниться
  expect(callback).toHaveBeenCalledWith('done');
});
  • Важно помнить, что tick() запускает только таймеры. Промисы внутри таймеров требуют отдельного ожидания через await или цепочку then.

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

  • Всегда вызывать uninstall() в afterEach(), чтобы избежать конфликтов между тестами.
  • Использовать tick() вместо реальных задержек, ускоряя тесты и снижая нестабильность.
  • При тестировании промисов в сочетании с таймерами важно учитывать микротаски, чтобы промисы успели выполниться после tick().
  • Фейковые таймеры работают не только с setTimeout и setInterval, но и с библиотеками, использующими стандартные таймеры, что делает их универсальным инструментом.

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