Spec-Zone.ru › Jest

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

Встроенные функции таймеров (то есть setTimeout(), setInterval(), clearTimeout(), clearInterval()) не слишком подходят для тестирования, поскольку их работа зависит от реального течения времени. Jest может заменить таймеры функциями, позволяющими управлять ходом времени. Великий Скотт!

информация

См. также документацию по API фиктивных таймеров.

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

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

function timerGame(callback) {
  console.log('Ready....go!');
  setTimeout(() => {
    console.log("Time's up -- stop!");
    callback && callback();
  }, 1000);
}

module.exports = timerGame;
timerGame.js
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

test('waits 1 second before ending the game', () => {
  const timerGame = require('../timerGame');
  timerGame();

  expect(setTimeout).toHaveBeenCalledTimes(1);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
__tests__/timerGame-test.js

Запуск всех таймеров

Ещё один тест, который мы можем написать для этого модуля, должен проверять, что функция обратного вызова вызывается через 1 секунду. Для этого мы воспользуемся API управления таймерами Jest, чтобы перемотать время вперёд прямо во время теста:

jest.useFakeTimers();
test('calls the callback after 1 second', () => {
  const timerGame = require('../timerGame');
  const callback = jest.fn();

  timerGame(callback);

  // At this point in time, the callback should not have been called yet
  expect(callback).not.toHaveBeenCalled();

  // Fast-forward until all timers have been executed
  jest.runAllTimers();

  // Now our callback should have been called!
  expect(callback).toHaveBeenCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Запуск ожидающих таймеров

В некоторых случаях у вас может быть рекурсивный таймер — то есть таймер, который устанавливает новый таймер в собственной функции обратного вызова. В таких случаях запуск всех таймеров приведёт к бесконечному циклу и вызову следующей ошибки: «Прерывание после запуска 100000 таймеров: предполагается бесконечный цикл!»

Если это ваш случай, проблему решит использование jest.runOnlyPendingTimers():

function infiniteTimerGame(callback) {
  console.log('Ready....go!');

  setTimeout(() => {
    console.log("Time's up! 10 seconds before the next game starts...");
    callback && callback();

    // Schedule the next game in 10 seconds
    setTimeout(() => {
      infiniteTimerGame(callback);
    }, 10000);
  }, 1000);
}

module.exports = infiniteTimerGame;
infiniteTimerGame.js
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

describe('infiniteTimerGame', () => {
  test('schedules a 10-second timer after 1 second', () => {
    const infiniteTimerGame = require('../infiniteTimerGame');
    const callback = jest.fn();

    infiniteTimerGame(callback);

    // At this point in time, there should have been a single call to
    // setTimeout to schedule the end of the game in 1 second.
    expect(setTimeout).toHaveBeenCalledTimes(1);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

    // Fast forward and exhaust only currently pending timers
    // (but not any new timers that get created during that process)
    jest.runOnlyPendingTimers();

    // At this point, our 1-second timer should have fired its callback
    expect(callback).toHaveBeenCalled();

    // And it should have created a new timer to start the game over in
    // 10 seconds
    expect(setTimeout).toHaveBeenCalledTimes(2);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
  });
});
__tests__/infiniteTimerGame-test.js
примечание

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

jest.useFakeTimers({timerLimit: 100});

Перемещение таймеров вперёд на заданное время

Также можно использовать jest.advanceTimersByTime(msToRun). При вызове этого API все таймеры перемещаются вперёд на msToRun миллисекунд. Будут выполнены все ожидающие «макрозадачи», поставленные в очередь с помощью setTimeout() или setInterval(), которые должны были бы выполниться в течение этого периода времени. Кроме того, если эти макрозадачи запланируют новые макрозадачи, которые должны выполниться в тот же период времени, они тоже будут выполнены, пока в очереди не останется макрозадач, которые нужно выполнить в течение msToRun миллисекунд.

function timerGame(callback) {
  console.log('Ready....go!');
  setTimeout(() => {
    console.log("Time's up -- stop!");
    callback && callback();
  }, 1000);
}

module.exports = timerGame;
timerGame.js
jest.useFakeTimers();
it('calls the callback after 1 second via advanceTimersByTime', () => {
  const timerGame = require('../timerGame');
  const callback = jest.fn();

  timerGame(callback);

  // At this point in time, the callback should not have been called yet
  expect(callback).not.toHaveBeenCalled();

  // Fast-forward until all timers have been executed
  jest.advanceTimersByTime(1000);

  // Now our callback should have been called!
  expect(callback).toHaveBeenCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});
__tests__/timerGame-test.js

Наконец, в некоторых тестах может быть полезно удалить все ожидающие таймеры. Для этого предусмотрен jest.clearAllTimers().

Перемещение таймеров вперёд до следующего кадра

В приложениях часто требуется запланировать выполнение кода внутри кадра анимации (с помощью requestAnimationFrame). Мы предоставляем удобный метод jest.advanceTimersToNextFrame(), который перемещает все таймеры вперёд на количество миллисекунд, достаточное для выполнения всех запланированных кадров анимации.

При имитации времени кадры анимации выполняются каждые 16ms (что соответствует примерно 60 кадрам в секунду) после запуска часов. Если запланировать функцию обратного вызова в кадре анимации (с помощью requestAnimationFrame(callback)), callback будет вызвана, когда часы продвинутся на 16ms. jest.advanceTimersToNextFrame() продвинет часы ровно настолько, чтобы достичь следующего шага 16ms. Если с момента планирования callback кадра анимации часы уже продвинулись на 6ms, то часы будут продвинуты на 10ms.

jest.useFakeTimers();
it('calls the animation frame callback after advanceTimersToNextFrame()', () => {
  const callback = jest.fn();

  requestAnimationFrame(callback);

  // At this point in time, the callback should not have been called yet
  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersToNextFrame();

  // Now our callback should have been called!
  expect(callback).toHaveBeenCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Выборочная подмена

Иногда вашему коду может потребоваться не переопределять исходную реализацию того или иного API. В этом случае можно воспользоваться параметром doNotFake. Например, вот как можно передать пользовательскую функцию-мок для performance.mark() в среде jsdom:

/**
 * @jest-environment jsdom
 */

const mockPerformanceMark = jest.fn();
window.performance.mark = mockPerformanceMark;

test('allows mocking `performance.mark()`', () => {
  jest.useFakeTimers({doNotFake: ['performance']});

  expect(window.performance.mark).toBe(mockPerformanceMark);
});

© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/timer-mocks

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API