Any ideas on this code
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback() // LINE-A without await here, test works as expected.
setTimeout(() => {
simpleTimer(callback)
}, 1000)
}
const callback = jest.fn()
await simpleTimer(callback)
jest.advanceTimersByTime(8000)
expect(callback).toHaveBeenCalledTimes(9)
}
```
Failed with
Expected mock function to have been called nine times, but it was called two times.
However, If I remove await
from LINE-A, the test passes.
Does Promise and Timer not work well?
I think the reason maybe jest is waiting for second promise to resolve.
Yes, you're on the right track.
What happens
await simpleTimer(callback)
will wait for the Promise returned bysimpleTimer()
to resolve socallback()
gets called the first time andsetTimeout()
also gets called.jest.useFakeTimers()
replacedsetTimeout()
with a mock so the mock records that it was called with[ () => { simpleTimer(callback) }, 1000 ]
.jest.advanceTimersByTime(8000)
runs() => { simpleTimer(callback) }
(since 1000 < 8000) which callssetTimer(callback)
which callscallback()
the second time and returns the Promise created byawait
.setTimeout()
does not run a second time since the rest ofsetTimer(callback)
is queued in thePromiseJobs
queue and has not had a chance to run.expect(callback).toHaveBeenCalledTimes(9)
fails reporting thatcallback()
was only called twice.Additional Information
This is a good question. It draws attention to some unique characteristics of JavaScript and how it works under the hood.
Message Queue
JavaScript uses a message queue. Each message is run to completion before the runtime returns to the queue to retrieve the next message. Functions like
setTimeout()
add messages to the queue.Job Queues
ES6 introduces
Job Queues
and one of the required job queues isPromiseJobs
which handles "Jobs that are responses to the settlement of a Promise". Any jobs in this queue run after the current message completes and before the next message begins.then()
queues a job inPromiseJobs
when the Promise it is called on resolves.async / await
async / await
is just syntactic sugar over promises and generators.async
always returns a Promise andawait
essentially wraps the rest of the function in athen
callback attached to the Promise it is given.Timer Mocks
Timer Mocks work by replacing functions like
setTimeout()
with mocks whenjest.useFakeTimers()
is called. These mocks record the arguments they were called with. Then whenjest.advanceTimersByTime()
is called a loop runs that synchronously calls any callbacks that would have been scheduled in the elapsed time, including any that get added while running the callbacks.In other words,
setTimeout()
normally queues messages that must wait until the current message completes before they can run. Timer Mocks allow the callbacks to be run synchronously within the current message.Here is an example that demonstrates the above information:
How to get Timer Mocks and Promises to play nice
Timer Mocks will execute the callbacks synchronously, but those callbacks may cause jobs to be queued in
PromiseJobs
.Fortunately it is actually quite easy to let all pending jobs in
PromiseJobs
run within anasync
test, all you need to do is callawait Promise.resolve()
. This will essentially queue the remainder of the test at the end of thePromiseJobs
queue and let everything already in the queue run first.With that in mind, here is a working version of the test: