Jest Mocking — Part 3: Timer
In this article series, we will take a look at how to mock with Jest.
Jest Mocking — Part 1: Function
Jest Mocking — Part 2: Module
Jest Mocking — Part 3: Timer
Jest Mocking — Part 4: React Component
You can find the codes in the article on Github.
In our daily lives, we often schedule certain tasks based on timers. For example, we may eat a meal 40 minutes from now, or take medication every 24 hours. We can also benefit timer functions in our code, such as setTimeout
and setInterval
, to perform timed actions. Today, we will look at how to mock functions that contain timer methods.
Introduction
Let’s assume we have a function that calls the function it receives as a parameter after 3 seconds.
// File: callAfterThreeSeconds.ts
export default function callAfterThreeSeconds(callback: () => void) {
console.log("before setTimeout");
setTimeout(() => {
console.log("before callback");
callback();
console.log("after callback");
}, 3000);
console.log("after setTimeout");
}
callAfterThreeSeconds(() => {
console.log("executed callback");
});
/* OUTPUT:
before setTimeout
after setTimeout
before callback
executed callback
after callback
*/
The function runs, the callback function is added to the event loop, and it is run after 3 seconds. Let’s write a test and using the knowledge in our hand. We have seen that we can mock methods on objects with jest.spyOn
. We can use the global object for built-in methods.
Don’t forget to delete lines 60–62 to prevent breaking our tests. Otherwise, you will see that it is called twice in the test, expecting it to be called once. If you want to refresh your knowledge about the event loop, you can also check out this video: What the heck is the event loop anyway? by Philip Roberts
// File: callAfterThreeSeconds.test.ts
import callAfterThreeSeconds from "./callAfterThreeSeconds";
jest.spyOn(global, "setTimeout");
test("should call callback after 3 second", () => {
const mockCallback = jest.fn();
callAfterThreeSeconds(mockCallback);
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 3000);
expect(mockCallback).toHaveBeenCalled();
});
/* OUTPUT:
console.log
before setTimeout
after setTimeout
FAIL callAfterThreeSeconds.test.ts
✕ should call callback after 3 second
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
*/
The first expect passes successfully, but the second fails because it did not expect the function to be completed. We need to specify in our test that the function will be completed in 3 seconds.
// File: callAfterThreeSeconds.test.ts
import callAfterThreeSeconds from "./callAfterThreeSeconds";
jest.spyOn(global, "setTimeout");
test("should call callback after 3 second", (done) => {
const mockCallback = jest.fn();
callAfterThreeSeconds(mockCallback);
setTimeout(() => {
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 3000);
expect(mockCallback).toHaveBeenCalled();
done();
}, 3000);
});
/* OUTPUT:
PASS callAfterThreeSeconds.test.ts
✓ should call callback after 3 second
*/
There is a simple math for async functions in tests: if the code waits, the test should wait too. If the callback will be run after 3 seconds, we can be sure it was called by running expect after 3 seconds. We will use the done
method to tell Jest when to finish the test.
However, there is a problem here. What if the expected time was not 3 seconds but 24 hours?
This will only save the day. Let’s move on to a real solution.
Fake Timer
setTimeout
, setInterval
etc. methods are based on a timer. To mock this in tests, you can use the jest.useFakeTimers(fakeTimersConfig?)
. It affects all files regardless of where it is called and clears all information from previous timers each time it is called. If we want to return to the real timer in part of the code, we can use jest.useRealTimers
.
// File: useTimers.test.ts
test("playground", () => {
jest.useFakeTimers();
console.log("FAKES\n-------");
console.log("setTimeout:", setTimeout.toString());
console.log("setInterval:", setInterval.toString());
console.log("Date.now:", Date.now.toString());
jest.useRealTimers();
console.log("REALS\n-------");
console.log("setTimeout:", setTimeout.toString());
console.log("setInterval:", setInterval.toString());
console.log("Date.now:", Date.now.toString());
});
/* OUTPUT:
FAKES
-------
setTimeout: function () {
return clock[method].apply(clock, arguments);
}
setInterval: function () {
return clock[method].apply(clock, arguments);
}
Date.now: function now() {
return target.clock.now;
}
REALS
-------
setTimeout: function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: ``window, repeat: false });
}
setInterval: function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: ``window, repeat: true });
}
Date.now: function now() { [native code] }
*/
If we want to customize the fake timer, we can provide an object that can take the following values:
advanceTimers (boolean|number)
: We can specify how much faster it will advance compared to the real timer. If set to true, it will advance 1ms if the real timer advances 1ms. If a number is provided, it will advance that many times faster than 1ms in real timer.doNotFake (Array)
: A list of methods that we do not want to use the fake timer.now (number|Date)
: The system time that the fake timers will use. By default, it takes the value ofDate.now()
, which is the current date.timerLimit
: Maximum number of timers that can be run withjest.runAllTimers
which will be discussed later.
// File: customUseTimers.test.ts
jest.useFakeTimers({
now: new Date(1999, 2, 21),
doNotFake: ["setInterval"],
});
test("playground", () => {
console.log("setTimeout:", setTimeout.toString());
console.log("setInterval:", setInterval.toString());
console.log("Date.now:", new Date(Date.now()));
});
/* OUTPUT:
setTimeout: function () {
return clock[method].apply(clock, arguments);
}
setInterval: function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: window, repeat: true });
}
Date.now: 1999-03-20T22:00:00.000Z
*/
Executing Callbacks Immediately
Timer methods’s callbacks are queued in the event loop and are pop from the queue and execute when the time is complete. In tests, we can bypass the timeout and run the callback immediately. Let’s look at the test we wrote with setTimeout
again.
// File: callAfterThreeSeconds.test.ts
import callAfterThreeSeconds from "./callAfterThreeSeconds";
jest.spyOn(global, "setTimeout");
test("should call callback after 3 second", (done) => {
const mockCallback = jest.fn();
callAfterThreeSeconds(mockCallback);
setTimeout(() => {
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 3000);
expect(mockCallback).toHaveBeenCalled();
done();
}, 3000);
});
/* OUTPUT:
PASS callAfterThreeSeconds.test.ts
✓ should call callback after 3 second (3035 ms)
*/
Even though the time might seem small, the accumulation of 3 seconds can cause the pipeline to take 1.5 hours. To run the callbacks without waiting for timeouts, we can use the jest.runAllTimers
method.
To check the number of timers that are waiting to be completed during a test, you can use the
jest.getTimerCount
method.
// File: runAllTimers.test.ts
import callAfterThreeSeconds from "./callAfterThreeSeconds";
jest.useFakeTimers();
jest.spyOn(global, "setTimeout");
test("should wait 10 second before call callback", () => {
const mockCallback = jest.fn();
console.log("number of fake timers remaining:", jest.getTimerCount());
callAfterThreeSeconds(mockCallback);
console.log("number of fake timers remaining:", jest.getTimerCount());
// make sure the callback is not called because
// the timers dont expire before runAllTimers
expect(mockCallback).not.toHaveBeenCalled();
jest.runAllTimers();
console.log("number of fake timers remaining:", jest.getTimerCount());
expect(mockCallback).toHaveBeenCalled();
});
/* OUTPUT:
number of fake timers remaining: 0
before setTimeout
after setTimeout
number of fake timers remaining: 1
before callback
after callback
number of fake timers remaining: 0
PASS runAllTimers.test.ts
✓ should wait 10 second before call callback (29 ms)
*/
We have two different methods for this:
jest.runAllTicks()
: Executes callbacks in the micro-task (process.nextTick) queue.jest.runAllTimers()
: Executes callbacks in the macro-task (setTimeout, setInterval, setImmediate) queue.
Executing Only Callbacks in the Queue
jest.runAllTimers()
executes the callbacks of the macro-tasks in the queue and the callbacks derived from them. It continues until the queue is emptied.
// File: setupTimeouts.ts
export default function setupTimeouts() {
setTimeout(() => {
console.log("called callback 1.");
setTimeout(() => {
console.log("called child callback 1.");
}, 5000);
}, 3000);
setTimeout(() => {
console.log("called callback 2.");
setTimeout(() => {
console.log("called child callback 2.");
setTimeout(() => {
console.log("called childest callback 1.");
}, 1000);
}, 1000);
}, 3000);
}
// File: runAllTimers.test.ts
import setupTimeouts from "./setupTimeouts";
test("playground", () => {
jest.useFakeTimers();
setupTimeouts();
jest.runAllTimers();
});
/* OUTPUT:
called callback 1.
called callback 2.
called child callback 2.
called childest callback 1.
called child callback 1.
*/
To see the event loop flow in the example, you can check out the example I added to the JavaScript Visualizer 9000.
jest.runOnlyPendingTimers
only executes the callbacks currently in the macro-task queue. Does not affect derivatives.
// File: runOnlyPendingTimers.test.ts
import setupTimeouts from "./setupTimeouts";
test("playground", () => {
jest.useFakeTimers();
setupTimeouts();
jest.runOnlyPendingTimers();
});
/* OUTPUT:
called callback 1.
called callback 2.
*/
Executing Callbacks by Advancing the Timer to Specific Time
If we don’t want to pass the timeouts of all timers, we can also advance the timer. Let’s analyze the output by advancing the fake timer by 4000ms in the example.
// File: setupTimeouts.ts
export default function setupTimeouts() {
setTimeout(() => {
console.log("called callback 1.");
setTimeout(() => {
console.log("called child callback 1.");
}, 5000);
}, 3000);
setTimeout(() => {
console.log("called callback 2.");
setTimeout(() => {
console.log("called child callback 2.");
setTimeout(() => {
console.log("called childest callback 1.");
}, 1000);
}, 1000);
setTimeout(() => {
console.log("called child callback 3.");
}, 1000);
}, 3000);
}
// File: advanceTimersByTime.test.ts
import setupTimeouts from "./setupTimeouts";
test("playground", () => {
jest.useFakeTimers();
setupTimeouts();
jest.advanceTimersByTime(4000);
});
/* OUTPUT:
called callback 1.
called callback 2.
called child callback 2.
called child callback 3.
*/
It executes callbacks which are collected cumulatively and match the given time, at the timeout.
Executing Callbacks Step by Step
jest.advanceTimersToNextTimer(step?)
enables callbacks to be run step by step.
// File: setupTimeouts.ts
export default function setupTimeouts() {
setTimeout(() => {
console.log("called callback 1.");
setTimeout(() => {
console.log("called child callback 1.");
}, 5000);
}, 3000);
setTimeout(() => {
console.log("called callback 2.");
setTimeout(() => {
console.log("called child callback 2.");
setTimeout(() => {
console.log("called childest callback 1.");
}, 1000);
}, 1000);
setTimeout(() => {
console.log("called child callback 3.");
}, 1000);
}, 3000);
}
// // File: advanceTimersToNextTimer.test.ts
import setupTimeouts from "./setupTimeouts";
test("playground", () => {
jest.useFakeTimers();
setupTimeouts();
console.log("Step 1:");
jest.advanceTimersToNextTimer();
console.log("Step 2:");
jest.advanceTimersToNextTimer();
console.log("Step 3:");
jest.advanceTimersToNextTimer();
});
/* OUTPUT:
Step 1:
called callback 1.
called callback 2.
Step 2:
called child callback 2.
called child callback 3.
Step 3:
called childest callback 1.
*/
As we can see, we can progress step by step. If we want to take more than one step at once instead of going one by one, we can use the step
parameter.
// File: advanceTimersToNextTimerWithStep.test.ts
import setupTimeouts from "./setupTimeouts";
test("playground", () => {
jest.useFakeTimers();
setupTimeouts();
console.log("Step 1-2: callbacks");
jest.advanceTimersToNextTimer(2);
});
/* OUTPUT:
Step 1-2:
called callback 1.
called callback 2.
called child callback 2.
called child callback 3.
*/
Extra Methods
Finally, let’s talk about three small methods:
jest.now()
: Returns the current date as ms. If a fake timer is used, it gives the fake timer value, otherwise the real timer value.jest.setSystemTime(now?: number | Date)
: Changes the system date within the test.jest.getRealSystemTime()
: Gives the current date.
// File: extraMethods.test.ts
test("playground", () => {
jest.useFakeTimers();
jest.setSystemTime(new Date(1999, 2, 21));
console.log("real date:", new Date(jest.getRealSystemTime()));
console.log("fake date:", new Date(jest.now()));
});
/* OUTPUT:
real date: 2022-11-15T03:32:20.747Z
fake date: 1999-03-20T22:00:00.000Z
*/
Practice
Let’s end the article with a small example. You can find the completed tests at Github repo, but I recommend trying to write yourself. The times when I learn a lot are often the times I find it most difficult.
// breakReminder.ts
export default function breakReminder(breakActivity: any) {
console.log("starting working...");
let breakCount = 0;
const breakTimer = setInterval(() => {
if (breakCount > 2) {
clearInterval(breakTimer);
console.log("ending working.");
} else {
breakActivity();
}
breakCount += 1;
}, 3000);
}
After the tests run, the output should look like this:
/* OUTPUT:
PASS src/breakReminder.test.ts
breakReminder() tests
✓ should start work on first iteration (3 ms)
✓ should call breakActivity on second, third and fourth iteration (1 ms)
✓ should end work on last iteration (6 ms)
*/
We talked about how to mock timers. In the next article, we will talk about react.