Simulateurs de temporisation
Les fonctions de temporisation natives (c'est-à-dire setTimeout()
, setInterval()
, clearTimeout()
, clearInterval()
) sont moins adaptées à un environnement de test, car elles dépendent du temps réel pour s'écouler. Jest peut remplacer les temporisateurs par des fonctions qui vous permettent de contrôler le temps qui passe. Great Scott !
Consultez aussi la documentation de l'API des faux temporisateurs.
Activer les faux temporisateurs
Dans l'exemple suivant, nous activons les faux temporisateurs en appelant jest.useFakeTimers()
. Ceci remplace l'implémentation originale setTimeout()
et d'autres fonctions de temporisation. Les temporisateurs peuvent être restaurés à leur comportement normal avec jest.useRealTimers()
.
function timerGame(callback) {
console.log('Prêt .... partez !');
setTimeout(() => {
console.log("Le temps est écoulé -- arrêtez !");
callback && callback();
}, 1000);
}
module.exports = timerGame;
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
test(''attend 1 seconde avant de mettre fin au jeu', () => {
const timerGame = require('../timerGame');
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
Exécuter tous les temporisateurs
Un autre test que nous pourrions vouloir écrire pour ce module est celui qui affirme que le callback est appelé après 1 seconde. Pour ce faire, nous allons utiliser les API de contrôle des temporisateurs de Jest pour avancer rapidement dans le temps au beau milieu du test :
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);
});
Exécuter les temporisateurs en attente
Il existe également des scénarios dans lesquels vous pouvez avoir un temporisateur récursif, c'est-à-dire un temporisateur qui définit un nouveau temporisateur dans son propre callback. Pour ces derniers, l'exécution de tous les temporisateurs serait une boucle sans fin, ce qui entraînerait l'erreur suivante : « Aborting after running 100000 timers, assuming an infinite loop! »
Si c'est votre cas, l'utilisation de jest.runOnlyPendingTimers()
résoudra le problème :
function infiniteTimerGame(callback) {
console.log('Prêt .... partez !');
setTimeout(() => {
console.log("Le temps est écoulé ! 10 secondes avant le début de la prochaine partie..." );
callback && callback();
// Planifie la prochaine partie dans 10 secondes
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
module.exports = infiniteTimerGame;
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
describe('infiniteTimerGame', () => {
test('planifie un minuteur de 10 secondes après 1 seconde', () => {
const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();
infiniteTimerGame(callback);
// A ce stade, il devrait y avoir un seul appel à
// setTimeout pour programmer la fin du jeu dans 1 seconde.
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);
});
});
Pour le débogage ou toute autre raison, vous pouvez modifier la limite des temporisateurs qui seront exécutés avant de lever une erreur :
jest.useFakeTimers({timerLimit: 100});
Avancer les temporisateurs par une durée
Une autre possibilité est d'utiliser jest.advanceTimersByTime(msToRun)
. Lorsque cette API est appelée, tous les temporisateurs sont avancés de msToRun
millisecondes. Toutes les « macro-tâches » en attente qui ont été mises en file d'attente via setTimeout() ou setInterval(), et qui devraient être exécutées pendant ce laps de temps, seront exécutées. En plus, si ces macro-tâches planifient de nouvelles macro-tâches qui devraient être exécutées dans le même laps de temps, celles-ci seront exécutées jusqu'à ce qu'il ne reste plus de macro-tâches dans la file d'attente qui doivent être exécutées dans les msToRun millisecondes.
function timerGame(callback) {
console.log('Prêt .... partez !');
setTimeout(() => {
console.log("Le temps est écoulé -- arrêtez !");
callback && callback();
}, 1000);
}
module.exports = timerGame;
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);
});
Enfin, il peut parfois être utile dans certains tests de pouvoir effacer tous les temporisateurs en attente. Pour cela, nous avons jest.clearAllTimers()
.
Selective Faking
Sometimes your code may require to avoid overwriting the original implementation of one or another API. If that is the case, you can use doNotFake
option. For example, here is how you could provide a custom mock function for performance.mark()
in jsdom environment:
/**
* @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);
});