I am trying to mock a setTimeout inside of a setInterval with Jest, but am seeing confusing behavior with async/await and I do not know whether it is a bug, or if I am doing something wrong.
Awaiting inside the async function passed to setInterval causes setTimeout to never run. If I remove the await in the function below, the setTimeout is successfully set.
See updated Reproduction Case in my Below Comment
// With await, setTimeout is never called
let interval;
async function check() {
await Promise.resolve(true); //does not execute the setTimeout below
setTimeout(() => {
console.log('Hello World');
clearInterval(interval);
}, 1000);
}
const testFunction = async () => {
interval = setInterval(check, 1000);
};
describe('Timers', () => {
beforeEach(() => {
jest.useFakeTimers();
});
it('should set a timeout that prints Hello World', async () => {
await testFunction();
jest.advanceTimersByTime(100000);
});
});
// Removing the await, the setTimeout is executed
let interval;
async function check() {
Promise.resolve(true);
setTimeout(() => {
console.log('Hello World');
clearInterval(interval);
}, 1000);
}
const testFunction = async () => {
interval = setInterval(check, 1000);
};
describe('Timers', () => {
beforeEach(() => {
jest.useFakeTimers();
});
it('should set a timeout that prints Hello World', async () => {
await testFunction();
jest.advanceTimersByTime(100000);
});
});
setTimeout to run after a promise resolves.
Does this work if you do not fake time? If it does, it's a bug here 馃檪
Hello @SimenB , thanks for taking a look! I've cleaned up my code and recreated a test that succeeds when not using fakeTimers, succeeds without the await when using fakeTimers, and then fails when awaiting inside the async callback of a setInterval when using fakeTimers. Let me know if there are any other details I could provide.
jest.setTimeout(10000);
let interval;
/**
* Check for a promise to return true before clearing an interval and setting a timeout
* @param {boolean} shouldAwaitRes - toggle whether to await inside the async callback
* @param {function} callback - mock function to call inside timeout
*/
const check = async (shouldAwaitRes, callback) => {
const res = shouldAwaitRes ? await Promise.resolve(true) : Promise.resolve(true);
if (res) {
clearInterval(interval);
setTimeout(() => {
callback();
}, 2000);
}
};
/**
* Set an interval with an async function that awaits a response
* @param {boolean} shouldAwaitRes - toggle whether to await inside the async callback
* @param {function} callback - mock function to pass into set timeout
*/
const setAsyncInterval = async (shouldAwaitRes, callback) => {
interval = setInterval(check, 1000, shouldAwaitRes, callback);
};
/**
* Wait 6 seconds for real timers to run
*/
const pause = async () => {
return new Promise(resolve => {
setTimeout(resolve, 6000);
});
};
// ----------------------------------
// Tests
describe('Timers', () => {
let callback;
beforeEach(() => {
callback = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('No Fake Timers Used', () => {
it('should call the callback inside the async setInterval callback when awaiting', async done => {
await setAsyncInterval(true, callback); // setting to true to await inside async callback
// Pause 6 seconds to let the timers runs
await pause();
expect(callback).toBeCalled(); // callback is called
done();
});
});
describe('Fake Timers', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
it('should call the callback inside the async setInterval callback when NOT awaiting', async done => {
await setAsyncInterval(false, callback); // setting to false to disable awaiting inside async callback
// Advance Time 6 seconds
jest.advanceTimersByTime(6000);
expect(callback).toBeCalled(); // callback is called
done();
});
// The below test fails
it('should call the callback inside the async setInterval callback when awaiting', async done => {
await setAsyncInterval(true, callback); // setting to true to enable awaiting inside async callback
// Advance Time 6 seconds
jest.advanceTimersByTime(6000);
expect(callback).toBeCalled(); // callback is NOT called
done();
});
});
});
@SimenB Are fake timers supposed to run the promise microtask queue after each timer callback (faked) call?
I have this issue as well.
Given a function that creates a setInterval inside. setInterval's callback is an async function, which awaits for something and then invokes another function (I'm testing the result of this invoked function).
In my tests I'm using runOnlyPendingTimers or advanceTimersByTime but by the time I do the expect await inside the function I test is triggered afterwards, so my assert fails.
I've tried adding await Promise.resolve() after advancing my timers but still nothing changes.
So, I don't think this is a bug, the timers are advanced but there's no way to wait for that await :(
Later edit:
If i stack
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(...)
This seems to make it work but I don't really want to do that.
Later later edit:
I made it work somehow, here's an example:
const testPromise= Promise.resolve({ yourData });
const mockFnInsideInterval = jest
.spyOn(module, 'test')
.mockImplementation(() => testPromise);
invokeWrappingFunction();
jest.advanceTimersByTime(...);
await testPromise;
expect(...);
This seems to make it work
@SimenB ran into this. Removed jest.useFakeTimers, issue was resolved. My workaround was:
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
afterEach(() => {
global.setTimeout.mockRestore();
});
it('test code', async () => {
global.setTimeout.mockImplementation(callback => callback());
await theMethodThatHasSetTimeoutWithAwaitInsideCallback();
expect(global.setTimeout).toBeCalledWith(expect.any(Function), 25000);
});
Same issue here.