Skip to main content
Versão: Próximo

Simulações de Temporizador

As funções nativas do temporizador (por exemplo, setTimeout(), setInterval(), clearTimeout(), clearInterval()) não são ideais para um ambiente de teste, pois dependem do tempo real para decorrer. Jest pode trocar temporizadores por funções que permitem controlar a passagem do tempo. Great Scott!

informação

Veja também a documentação da API Fake Timers.

Habilitar Fake Timers

No exemplo a seguir, habilitamos os fake timers chamando jest.useFakeTimers(). Isso substituirá a implementação original de setTimeout() e outras funções de timer. Timers can be restored to their normal behavior with jest.useRealTimers().

__tests__/timerGame-test.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);
});

Executar todos os temporizadores

Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test:

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

timerGame(callback);

// // Neste momento, o retorno de chamada ainda não deveria ter sido chamado
expect(callback).not.toHaveBeenCalled();

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

// Agora nosso retorno de chamada deveria ter sido chamado!
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

Executar Temporizadores Pendentes

Também há cenários em que você pode ter um timer recursivo – ou seja, um timer que define um novo timer em seu próprio retorno de chamada. Para estes, executar todos os temporizadores seria um loop infinito, gerando o seguinte erro: "Abortando após executar 100.000 temporizadores, assumindo um loop infinito!"

Se for esse o seu caso, usar jest.runOnlyPendingTimers() resolverá o problema:

infiniteTimerGame.js
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;
__tests__/infiniteTimerGame-test.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);

// Neste ponto, deveria ter havido uma única chamada para
// setTimeout para agendar o fim do jogo em 1 segundo.
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);
});
});
note

For debugging or any other reason you can change the limit of timers that will be run before throwing an error:

jest.useFakeTimers({timerLimit: 100});

Avance Temporizadores por Tempo

Outra possibilidade é usar o jest.advanceTimersByTime(msToRun). When this API is called, all timers are advanced by msToRun milliseconds. When this API is called, all timers are advanced by msToRun milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed.

__tests__/timerGame-test.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);
});

Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have jest.clearAllTimers().

Advance Timers to the next Frame

In applications, often you want to schedule work inside of an animation frame (with requestAnimationFrame). We expose a convenience method jest.advanceTimersToNextFrame() to advance all timers enough milliseconds to execute all actively scheduled animation frames.

For mock timing purposes, animation frames are executed every 16ms (mapping to roughly 60 frames per second) after the clock starts. When you schedule a callback in an animation frame (with requestAnimationFrame(callback)), the callback will be called when the clock has advanced 16ms. jest.advanceTimersToNextFrame() will advance the clock just enough to get to the next 16ms increment. If the clock has already advanced 6ms since a animation frame callback was scheduled, then the clock will be advanced by 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);
});

Fake seletivo

Algumas vezes, seu código pode precisar evitar a sobrescrita da implementação de outra API. Se esse for o caso, você pode usar a opção doNotFake. Por exemplo, aqui é como você poderia fornecer uma função de simulação personalizada para performance.mark() em um ambiente 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);
});