Use the native Promise implementation breaks useFakeTimers
Steps to reproduce the behavior:
jest.useFakeTimers();
test('timing', () => {
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timer'), 100);
jest.runAllTimers();
console.log('end');
});
It should log:
This is because runAllTimers
should trigger the async promise handler first, then the timeout delayed by 100ms, then return control.
The workaround I've found is to add:
global.Promise = require('promise');
to my setup.js
Hey @ForbesLindesay, thanks for filing and for the workaround 馃憣
Is this because native promises don't use timers under the hood like a library has to?
Fake timers in Jest does not fake promises (yet: #6876), however - as you discovered - if you use a polyfill for Promise
that uses either setImmediate
or process.nextTick
as its implementation, it'll work. I think this is working as intended (for now)?
@SimenB that's my thought as well
I think you're right about why this doesn't work, but I don't think you're really right about this working as intended. Promises are a form of timer, and because in the past everyone was using polyfills, this used to work and only recently stopped working.
The fact people use promises differently isn't Jest's responsibility - it was never a feature of Jest that you could run Promises using its fake timers. As you've noted, polyfilling it with something that uses timers as its implementation makes it work again
The fact people use promises differently
i'm not clear on what you mean by this?
it was never a feature of Jest that you could run Promises using its fake timers
when fake timers were created, native Promises didn't exist. The intention was always that as new forms of "timer" were added to the language, fake timers would support them. For example, setImmediate
is not a timer, but is supported. As you can see on https://repl.it/repls/PhysicalBriefCores
The goal of jest has, for a long time, included being easy to use and un-surprising. This is very surprising behaviour for fake timers to have.
I would like to expand on this issue since it gets amplified by uses of setTimeouts within the async code:
jest.useFakeTimers();
test('timing', async () => {
const shouldResolve = Promise.resolve()
.then(() => console.log('before-promise'))
.then(() => new Promise(r => setTimeout(r, 20)))
.then(() => console.log('after-promise'));
setTimeout(() => console.log('timer'), 100);
jest.runAllTimers();
await shouldResolve;
console.log('end');
});
Timeout - Async callback was not invoked within the 30000ms timeout specified by jest.setTimeout.
Expected: before-promise -> after-promise -> timer -> end
Actual: timer -> before-promise -> Hangs
This issue here is there is nothing to continuously advance the timers once you're within the promise world. shouldResolve will never resolve.
Switching to global.Promise = require('promise');
does seem like does the trick to resolve the issue for this particular use case. However in practice we have found the it does not work for all use-cases.
The best solution without replacing promises that i have come up for this is a utility function to continuouslyAdvanceTimers. However your results will still be out of order.
const _setTimeout = global.setTimeout;
function continuouslyAdvanceTimers() {
let isCancelled = false;
async function advance() {
while (!isCancelled) {
jest.runOnlyPendingTimers();
await new Promise(r => _setTimeout(r, 1));
}
}
advance();
return () => {
isCancelled = true;
};
}
jest.useFakeTimers();
test('timing', async () => {
const shouldResolve = Promise.resolve()
.then(() => console.log('before-promise'))
.then(() => new Promise(r => setTimeout(r, 20)))
.then(() => console.log('after-promise'));
setTimeout(() => console.log('timer'), 100);
const cancelAdvance = continuouslyAdvanceTimers();
await shouldResolve;
cancelAdvance();
console.log('end');
});
Expected: before-promise -> after-promise -> timer -> end
Actual: timer -> before-promise -> after-promise -> end
@KamalAman
test('timing', async () => {
const shouldResolve = Promise.resolve()
.then(() => console.log('before-promise'))
.then(() => new Promise(r => setTimeout(r, 20)))
.then(() => console.log('after-promise'));
setTimeout(() => console.log('timer'), 100);
await Promise.resolve()
jest.runAllTimers()
await shouldResolve
console.log('end');
});
I don't think there's any point adding to this issue. The problem is clearly stated and defined. All this needs is for one of the jest maintainers to acknowledge that this is not working as intended, then someone can submit a patch to fix it.
It would be good if the "Needs more info" tag could be removed, since this quite clearly doesn't need more info.
Please refrain from "me-too" style comments.
I love this issue, really. After one day sucking I found this and it works now. Miracle. Incredible that I have to do hacks like this to test an async functionality with a test framework that supports async.
UPDATE. Example fix:
while (!fixture.storageMock.update.mock.calls.length) {
await Promise.resolve();
}
Note that it is impossible, by JavaScript spec, for an async function
to return anything other than native promises, so there's not anything we can do generically in Jest. This has to be solved in the engines themselves. You can do what #6876 documents (transpile everything), but that's not something Jest can decide to do for you.
See e.g. https://github.com/petkaantonov/bluebird/issues/1434 and https://github.com/sinonjs/lolex/issues/114. Your best bet is probably to follow the Lolex issue - both because Jest is going to move its fake timers implementation to be backed by Lolex, but also because Ben actually maintains Node, so any news on what would allow async
functions to function (hah) correctly when faked is probably gonna be posted there.
If at some point there is a way to return custom Promise from async functions in Node, then we can look into adding APIs for it in Jest. Until then, we're _unlikely_ to do anything
For what it's worth, we have resorted to overwriting window.setTimeout
when using a setTimeout
in a promise chain:
// Place this in the test file/test block when you want to immediately invoke
// the callback to setTimeout
window.setTimeout = (fn: () => void, _timeout: number): void => fn()
Posting this work around in case it helps someone else:
await Promise.resolve().then(() => jest.advanceTimersByTime(milliseconds));
More context here:
https://stackoverflow.com/questions/51126786/jest-fake-timers-with-promises/51132058#51132058
Broader example:
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export async function foo(fn: () => T, waitMs: number): Promise<T> {
await sleep(waitMs);
return fn();
}
it('calls fn after x milliseconds', async () => {
jest.useFakeTimers();
const fn = jest.fn(() => 3);
const retVal = foo(fn, 1000);
expect(fn).not.toBeCalled();
await Promise.resolve().then(() => jest.advanceTimersByTime(1000));
expect(fn).toHaveBeenCalledTimes(1);
await expect(retVal).resolves.toBe(3);
});
For those looking for the solution to this problem when using jest.useFakeTimers("modern");
https://github.com/facebook/jest/issues/10221#issuecomment-654687396
Most helpful comment
I would like to expand on this issue since it gets amplified by uses of setTimeouts within the async code:
Timeout - Async callback was not invoked within the 30000ms timeout specified by jest.setTimeout.
Expected: before-promise -> after-promise -> timer -> end
Actual: timer -> before-promise -> Hangs
This issue here is there is nothing to continuously advance the timers once you're within the promise world. shouldResolve will never resolve.
Switching to
global.Promise = require('promise');
does seem like does the trick to resolve the issue for this particular use case. However in practice we have found the it does not work for all use-cases.The best solution without replacing promises that i have come up for this is a utility function to continuouslyAdvanceTimers. However your results will still be out of order.
Expected: before-promise -> after-promise -> timer -> end
Actual: timer -> before-promise -> after-promise -> end