Reputation: 3389
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.
Upvotes: 92
Views: 72342
Reputation: 2903
Brian Adams's answer is spot on.
But calling await Promise.resolve()
only seems to resolve one pending promise.
In the real world, testing functions having multiple asynchronous calls would be painful if we have to call this expression over and over again per iteration.
Instead, if your function has multiple await
s it's easier to do this:
Create this function somewhere
Jest < v27
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
Jest >= v27
function flushPromises() {
return new Promise(resolve =>
jest.requireActual('timers').setImmediate(resolve)
);
}
Now call await flushPromises()
wherever you would have otherwise called multiple await Promise.resolve()
s
More details on this GitHub issue.
Upvotes: 75
Reputation: 1
Some further clarification to Molten Ice's comments on Promise.resolve():
Promise.resolve() will clear the (FIFO) microtask queue of work, but will not cause execution of any callbacks created by promise chaining with .then. That is because those callbacks will be placed on the microtask queue after the promise they are called on settles, and after execution of the synchronous code following await Promise.resolve()
.
This behaviour can be demonstrated as follows:
Promise.resolve().then(() => console.log(1)).then(() => console.log(2)).then(() => console.log(3)).then(() => console.log(4));
Promise.resolve().then(() => console.log(5)).then(() => console.log(6)).then(() => console.log(7)).then(() => console.log(8));
Promise.resolve().then(() => console.log(9)).then(() => console.log(10)).then(() => console.log(11)).then(() => console.log(12));
will output
1
5
9
2
6
10
3
7
11
4
8
12
Upvotes: 0
Reputation: 4705
Since Jest v28+, simply passing advanceTimers
option as in:
jest.useFakeTimers({advanceTimers: true})
will do to make fake timers and pending promises work in harmony :-)
From the docs (https://jestjs.io/docs/jest-object#jestusefaketimersfaketimersconfig):
type FakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
*/
advanceTimers?: boolean | number;
// ...
}
Upvotes: 1
Reputation: 8377
Since Jest v29.5.0 You could use jest.advanceTimersByTimeAsync(msToRun)
Asynchronous equivalent of jest.advanceTimersByTime(msToRun). It allows any scheduled promise callbacks to execute before running the timers.
...
await jest.advanceTimersByTimeAsync(1000);
expect(callback).toHaveBeenCalled
Upvotes: 12
Reputation: 49
I have a retry with timeouts pattern: awaiting a promise with a timeout several times. I ended up with the following solution, based on Brian Adams' answer, if it can be of any help to anyone.
/**
* Execute an async function while flushing timers in a loop as long as the promise is still pending
*
* @param fn an async function
* @returns fn return type
*
* @see {@link https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function}
*/
const executeWhileFlushingPromisesAndTimers = async <F extends () => Promise<any>>(
fn: F,
maxLoopNb = 100,
): Promise<Awaited<ReturnType<F>>> => {
let pending = true
let result: Awaited<ReturnType<F>>
let error: Error
let times = 0
fn()
.then((res) => {
result = res
pending = false
})
.catch((err) => {
error = err
})
.finally(() => {
pending = false
})
while (pending && times < maxLoopNb) {
await Promise.resolve()
jest.runAllTimers()
await new Promise((resolve) => (jest.requireActual('timers') as any).setTimeout(resolve, 0))
times++
}
if (pending) {
throw new Error(
`From executeFlushingPromisesAndTimers - promise still pending after ${maxLoopNb} (maxLoopNb) jest.runAllTimes. Make sure to mock the asynchronous code.`,
)
}
if (error) {
throw error
}
return result
}
test('async retry with timeout', () => {
expect(await executeWhileFlushingPromisesAndTimers(initSW)).toBe(false)
})
Upvotes: 1
Reputation: 7018
I prefer to use my own fake timer in complex tests.
export const useCustomTimer = () => {
var time = 0;
var timers: {callback: () => void, ms: number}[] = [];
const setCustomTimer = (callback: () => void, ms: number = 0) => {
if(ms<=time){
callback();
return;
}
timers.push({callback, ms})
timers.sort((a,b) => a.ms - b.ms);
}
const advanceTimersByTime = (ms: number) => {
time += ms;
timers = timers.reduce((acc, val) => {
if(val.ms<=time) {
val.callback();
}
else acc.push(val);
return acc;
}, []);
}
const advanceTimersToNextTimer = () => {
if(timers.length) advanceTimersByTime(timers[0].ms - time);
}
return {
setCustomTimer,
advanceTimersByTime,
advanceTimersToNextTimer
}
}
Test:
test('should demonstrate custom timer', async () => {
const {setCustomTimer, advanceTimersByTime, advanceTimersToNextTimer} = useCustomTimer();
const values = [];
values.push(0);
const promiseAll = Promise.all([
new Promise<void>((res) => setCustomTimer(() => { values.push(2); res(); }, 5)),
new Promise<void>((res) => setCustomTimer(() => { values.push(4); res(); }, 12)),
new Promise<void>((res) => setCustomTimer(() => { values.push(6); res(); }, 20)),
])
.then(() => {
values.push(7);
})
values.push(1);
advanceTimersToNextTimer(); // OR advanceTimersByTime(5);
values.push(3);
advanceTimersToNextTimer(); // OR advanceTimersByTime(7);
values.push(5);
advanceTimersToNextTimer(); // OR advanceTimersByTime(8);
await promiseAll;
values.push(8);
expect(values).toEqual([ 0, 1, 2, 3, 4, 5, 6, 7, 8]);
})
Upvotes: 1
Reputation: 21
I stumbled on the same issue and ended up using @sinonjs/fake-timers
directly, since it provides clock.tickAsync()
function, which according to the docs:
The tickAsync() will also break the event loop, allowing any scheduled promise callbacks to execute before running the timers.
The working example now becomes:
const FakeTimers = require('@sinonjs/fake-timers');
const clock = FakeTimers.install()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback()
setTimeout(() => {
simpleTimer(callback)
}, 1000)
}
const callback = jest.fn()
await simpleTimer(callback)
await clock.tickAsync(8000)
expect(callback).toHaveBeenCalledTimes(9) // SUCCESS \o/
});
Upvotes: 2
Reputation: 2788
The above was really helpful! For those who are trying to do this with React hooks(!) the following code worked for us:
// hook
export const useApi = () => {
const apis = useCallback(
async () => {
await Promise.all([
new Promise((resolve) => {
api().then(resolve);
}),
new Promise((resolve) => {
return setTimeout(() => {
resolve();
}, 10000);
}),
]);
},
[],
);
return [apis];
}
// test
import { renderHook, act } from '@testing-library/react-hooks';
function flushPromises() {
return new Promise((resolve) => setImmediate(resolve))
}
it('tests useApi', async () => {
jest.useFakeTimers();
const { result } = renderHook(() => useApi());
api.mockReturnValue(Promise.resolve());
await act(async () => {
const promise = result.current[0]()
await flushPromises()
jest.runAllTimers()
return promise
})
});
Upvotes: 1
Reputation: 12718
There is a use case I just couldn't find a solution:
function action(){
return new Promise(function(resolve, reject){
let poll
(function run(){
callAPI().then(function(resp){
if (resp.completed) {
resolve(response)
return
}
poll = setTimeout(run, 100)
})
})()
})
}
And the test looks like:
jest.useFakeTimers()
const promise = action()
// jest.advanceTimersByTime(1000) // this won't work because the timer is not created
await expect(promise).resolves.toEqual(({completed:true})
// jest.advanceTimersByTime(1000) // this won't work either because the promise will never resolve
Basically the action won't resolve unless the timer advances. Feels like a circular dependency here: promise need timer to advance to resolve, fake timer need promise to resolve to advance.
Upvotes: 4
Reputation: 45810
Yes, you're on the right track.
await simpleTimer(callback)
will wait for the Promise returned by simpleTimer()
to resolve so callback()
gets called the first time and setTimeout()
also gets called. jest.useFakeTimers()
replaced setTimeout()
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 calls setTimer(callback)
which calls callback()
the second time and returns the Promise created by await
. setTimeout()
does not run a second time since the rest of setTimer(callback)
is queued in the PromiseJobs
queue and has not had a chance to run.
expect(callback).toHaveBeenCalledTimes(9)
fails reporting that callback()
was only called twice.
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 is PromiseJobs
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 in PromiseJobs
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 and await
essentially wraps the rest of the function in a then
callback attached to the Promise it is given.
Timer Mocks
Timer Mocks work by replacing functions like setTimeout()
with mocks when jest.useFakeTimers()
is called. These mocks record the arguments they were called with. Then when jest.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:
jest.useFakeTimers();
test('execution order', async () => {
const order = [];
order.push('1');
setTimeout(() => { order.push('6'); }, 0);
const promise = new Promise(resolve => {
order.push('2');
resolve();
}).then(() => {
order.push('4');
});
order.push('3');
await promise;
order.push('5');
jest.advanceTimersByTime(0);
expect(order).toEqual([ '1', '2', '3', '4', '5', '6' ]);
});
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 an async
test, all you need to do is call await Promise.resolve()
. This will essentially queue the remainder of the test at the end of the PromiseJobs
queue and let everything already in the queue run first.
With that in mind, here is a working version of the test:
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback();
setTimeout(() => {
simpleTimer(callback);
}, 1000);
}
const callback = jest.fn();
await simpleTimer(callback);
for(let i = 0; i < 8; i++) {
jest.advanceTimersByTime(1000);
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
expect(callback).toHaveBeenCalledTimes(9); // SUCCESS
});
Upvotes: 194