I'm using Jest fake timers and have a test that relies on advancing both the fake time and real time. What is the correct way of doing this?
I must advance the fake time in order to trigger a cron to schedule new tasks (which should for example happen every 10 minutes). I must advance the real time because I'm persisting things to the database.
The test looks like this:
describe('Rescheduling expired jobs', () => {
beforeAll(async () => {
jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] });
});
it('Jobs are automatically rescheduled', async () => {
// ... Write expired test jobs in the database
await advanceTimersByTime(RESCHEDULE_EXPIRED_JOB_EVERY_MS);
// ... Read database jobs and expect them to be rescheduled
});
});
export async function advanceTimersByTime(msToRun: number) {
jest.advanceTimersByTime(msToRun);
await flushPromises();
}
// patch from github.com/jestjs/jest/issues/2157#issuecomment-897935688
function flushPromises() {
return new Promise(jest.requireActual('timers').setImmediate);
}
Remarks:
typeorm
's await BaseEntity.save()
.typeorm
's await BaseEntity.reload()
.But when I read data from the database, I sometimes get the correct state, some other time I don't (its more or less random). At first I though I was missing an await
somewhere, but then I double checked all my database writes and they were all good. So I guess Jest is doing some strange stuff and actually have two event loops (one for the tests and one for the application code?). Anyway, waiting for real time to advance seemed to be the only way to get this to work so I first tried this:
// ... Write expired test jobs in the database
await advanceTimersByTime(RESCHEDULE_EXPIRED_JOB_EVERY_MS);
await sleep(1);
// ... Read database jobs and expect them to be rescheduled
With sleep defined as such:
async function sleep(ms: number){
await new Promise((res) => setTimeout(res, ms));
};
This failed. Every time sleep is called in a test, the test gets a timeout error. I think its because useFakeTimers
do fake setTimeout
which makes this attempt at advancing real time worthless (and I can't add it to the doNotFake
list because of the cron).
So I decided to just replace the asynchronous sleep function by a synchronous elapseRealTime
function call that simply loops over and over until the time is right:
function elapseRealTime(ms: number) {
const start = new Date().getTime();
for (let _ = 0; _ < 1e7; _++) {
if (new Date().getTime() - start > ms) {
break;
}
}
}
That solution works but seems a little far-fetched to me. I must have misunderstood something, or missing something because I don't know much about jest. Note that using this requires to not mock Date
:
jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate', 'Date'] });
What is the correct way of dealing with this? Why is jest spawning two even loops (is it really)? Or maybe there is a bug in typeorm's save that doesn't actually wait for data to be persisted?
Any help/suggestion is welcome.
I found a way, that is also dirty, but I find it a little bit less dirty. It seems like setInterval
is not internally using setTimeout
therefore I can exclude setInterval
from the mock list:
jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate', 'setInterval'] });
Which I can then use to build a sleep function:
async function sleep(ms: number) {
let intervalId: number;
return new Promise<void>((resolve) => {
intervalId = setInterval(async () => {
resolve();
clearInterval(intervalId); // Cancels itself on first run => mimic setTimeout
}, ms);
});
}
At least now it's not an active "pause", but I'm lucky that the cron doesn't seem to rely on setInterval
to function properly.