Code360 powered by Coding Ninjas X Naukri.com. Code360 powered by Coding Ninjas X Naukri.com
Table of contents
1.
Introduction
2.
Timer Mocks
3.
Run All Timers
4.
Run Pending Timers
5.
Advance Timers By Time
6.
FAQs
7.
Key Takeaways
Last Updated: Mar 27, 2024
Easy

Using Fake Timers

Author Toohina Barua
0 upvote

Introduction

Your tests may become unpredictable, slow, and flaky if your code uses timers. 
Most testing frameworks allow you to replace real timers in your tests with fake ones to solve these problems or if you need to rely on specific timestamps in your code. Because it has some overhead, it should only be used on occasion and not on a regular basis.
When you use fake timers in your tests, they are used throughout the entire code. We will learn how to use fake timers from this article. So let's get started.

Timer Mocks

Because they rely on real time to elapse, the native timer functions (setTimeout, setInterval, clearTimeout, clearInterval) are not ideal for a testing environment. Timers can be replaced with functions that allow you to control the passage of time in Jest.

timerGame.js

'use strict';


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


module.exports = timerGame;


__tests__/timerGame-test.js

'use strict';

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);
});


By calling jest.useFakeTimers(), we enable fake timers. With mock functions, setTimeout and other timer functions are mocked out. With jest.useRealTimers(), timers can be restored to their original state.

While you can use jest.useFakeTimers() or jest.useRealTimers() from anywhere (top level, it block, etc. ), it is a global operation that will affect other tests in the same file. Before each test, you must also call jest.useFakeTimers() to reset internal counters. If you don't plan to use fake timers in all of your tests, you'll need to clean up manually, as the faked timers will leak across tests otherwise:

afterEach(() => {
 jest.useRealTimers();
});


test('do something with fake timers', () => {
 jest.useFakeTimers();
 // ...
});


test('do something with real timers', () => {
 // ...
});


There are currently two implementations of fake timers available: modern and legacy, with modern being the default.

Run All Timers

Another test for this module that we should write is one that asserts the callback is called after 1 second. To accomplish this, we'll use Jest's timer control APIs to jump ahead in time 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);


 // At this point in time, the callback shouldn’t have been called yet
 expect(callback).not.toBeCalled();


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


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

Run Pending Timers

There are also situations where a recursive timer is used, which is a timer that sets a new timer in its own callback. Running all of the timers would result in an infinite loop, resulting in the following error:

Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out…


As a result, something like jest.runAllTimers() isn't a good idea. You could use jest.runOnlyPendingTimers() in these cases:

infiniteTimerGame.js

'use strict';


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


 setTimeout(() => {
   console.log("Time's up!There are 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
'use strict';


jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');


describe('infiniteTimerGame', () => {
 test('schedules a 10 seconds 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).toBeCalled();


   // 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);
 });
});

Advance Timers By Time

Use jest.advanceTimersByTime(msToRun) as another option. All timers are advanced by msToRun milliseconds when this API is called. All pending "macro-tasks" that have been queued via setTimeout() or setInterval() and are scheduled to run during this time period will be completed. Furthermore, if those macro-tasks schedule new macro-tasks to run within the same time frame, those will be executed until there are no more macro-tasks in the queue that should be run within msToRun milliseconds.

timerGame.js

'use strict';


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


module.exports = timerGame;


__tests__/timerGame-test.js

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


 timerGame(callback);


 // At this point in time, the callback shouldn’t have been called yet
 expect(callback).not.toBeCalled();


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


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


Finally, having the ability to clear all pending timers may be useful in some tests. We have jest.clearAllTimers() for this.

FAQs

  1. Why do you need to restore the timers after your test runs?
    The main reason for this is to avoid coupling 3rd party libraries running after your test finishes (e.g. cleanup functions) to your fake timers and instead use real timers.
  2. What do you use to restore the timers?
    To restore the timers you usually call useRealTimers in afterEach.
  3. Why is it important to also call runOnlyPendingTimers before switching to real timers?
    This will ensure that all pending timers are flushed before switching to real timers. If you don't advance the timers and instead use real timers, the scheduled tasks will not be completed, resulting in unexpected behaviour. This is especially important for third parties who schedule tasks without your knowledge.
  4. Why do we use fake timers?
    The tests may become unpredictable, slow, and flaky if the code does not use fake timers.
  5. How can you clear all the pending timers?
    It may be useful in some tests to be able to use jest.clearAllTimers() to clear all of the pending timers.

Key Takeaways

In this article, we have extensively discussed the theoretical and practical implementation of Fake Timers.
We hope that this blog has helped you enhance your knowledge regarding the theoretical and practical implementation of Fake Timers and if you would like to learn more, check out our articles on Coding Ninjas Studio. Do upvote our blog to help other ninjas grow. Happy Coding!

Live masterclass