I've struggled with this for a while because questions/answers about this problem are often unprecise.
In the case where you are running jest with jest.useFakeTimers(), you have then have two types of time:
- Real time (wall clock), that one can't be faked.
- Fake time (mocked) that is resulting from the jest call to
useFakeTimers.
In this case, doing something "after a delay" isn't that clear. Are we talking about a real delay, or a fake delay?
Advancing fake time
This is the easier part it has been answered in many SO posts and github issues, for example, patching calls to advanceTimersByTime:
export async function advanceTimersByTime(msToRun: number) {
jest.advanceTimersByTime(msToRun);
await flushPromises();
}
function flushPromises() {
return new Promise(jest.requireActual('timers').setImmediate);
}
Which can then be used to check if a long lasting task inside your app managed to perform correctly:
describe('Tests', () => {
it('async test', async () => {
startLongTask();
await advanceTimersByTime(10_MINUTES);
// Assert everything went according to specifications
});
});
Advancing real time (when using fake timers)
The problem arise when you want to advance real time after you called jest.useFakeTimers(). I've tried many approaches, here is the one that worked for me.
In my use case (testing node-cron based scheduler) I had no need for setInterval to be mocked by jest, therefore, I could simply tell jest to not patch it:
jest.useFakeTimers({
doNotFake: ['nextTick', 'setImmediate', 'setInterval'],
});
Remarak, using nextTick seems to be required (otherwise tests will timeout).
Using this, setInterval is now based on real time, which will allow us to define an elapseRealTime function using it:
export async function elapseRealTime(ms: number) {
return new Promise<void>((resolve) => {
const intervalId = setInterval(async () => {
resolve();
clearInterval(intervalId); // mimic setTimeout
}, ms);
});
}
This sounds dumb, but it works. Now we can use elapseRealTime in a test to advance real time.
describe('Tests', () => {
it('async test', async () => {
startBackgroundTask();
await advanceTimersByTime(10_MINUTES);
// Eventhough fake time now advanced 10 minutes, the background task
// had no time to be executed if its not part of our app
// (it is using real time and is unaware of our fake timers)
await elapseRealTime(10_MS);
// Now the background task had 10 milliseconds to perform some
// work.
});
});
If you need to mock setIntervall in your tests
In the case where you need setIntervall to be mocked, an alternative solution (that I don't like that much) is to not mock Date:
jest.useFakeTimers({
doNotFake: ['nextTick', 'setImmediate', 'Date'],
});
And use an active loop that regularly check the current time. In our case, this can then be used to advance real time:
function elapseRealTime(ms: number) {
const now = new Date().getTime();
const then = now + ms;
while(new Date().getTime() < then){
/* Do nothing */
}
}
Since this one is synchronous, it will lock every other test out of the event loop (so it will cost you CPU time and make your test suite run for longer). That also mean you need to call it without await:
describe('Tests', () => {
it('async test', async () => {
startBackgroundTask();
await advanceTimersByTime(10_MINUTES);
elapseRealTime(10_MS);
});
});
Why would you need both types of delays
You might need to wait for a fake delay if you are testing so internal application time-related events (a cron scheduling a given task after a delay for example).
But you might also be interested in real delay for example to give enough time for another application (or a database/sensor/...) to achieve a task related to the test you are running (integration tests). In my use case I needed another app to trigger web hooks calls.