jest: useFakeTimers breaks with native promise implementation
š Bug Report
Use the native Promise implementation breaks useFakeTimers
To Reproduce
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');
});
Expected behavior
It should log:
- promise
- timer
- end
This is because runAllTimers
should trigger the async promise handler first, then the timeout delayed by 100ms, then return control.
Actual Behaviour
- timer
- end
- promise
Link to repl or repo (highly encouraged)
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 37
- Comments: 23 (9 by maintainers)
Commits related to this issue
- attempted to change the global promise based on https://github.com/facebook/jest/issues/7151#issuecomment-463370069 — committed to zhughes3/jest-timer-testing by zhughes3 4 years ago
- Replace setTimeout function in LastFM getPage tests Jest fake timers don't play well with promises and setTimeout so we replace the setTimeout function and don't have to wait for the real timeout. Se... — committed to metabrainz/listenbrainz-server by MonkeyDo 3 years ago
- Set retry limit for LastFMImporter (#1339) * Set retry limit for LastFMImporter * Export LASTFM_RETRIES and use in tests * Document getPage and remove magic number 4 * Await on retry * Re... — committed to metabrainz/listenbrainz-server by amCap1712 3 years ago
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
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:
@KamalAman
iām not clear on what you mean by this?
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/PhysicalBriefCoresThe 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.
The workaround Iāve found is to add:
to my setup.js
@tatethurston I hate that this solution works, but it does. š Thank you.
Thanks for this information. When I used it with @KamalAman 's solution it worked perfectly.
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
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
Thanks for this thread and particular comment ā¤ļø I also hate that it works, but it does š Also could be wrapped into helper function š¤
So in tests it could be:
Since
@sinon/fake-timers
has async versions of all timer-advancing methods designed to also run microtasks (https://github.com/sinonjs/fake-timers/pull/237), could this functionality be exposed in jest to solve this issue?For what itās worth, we have resorted to overwriting
window.setTimeout
when using asetTimeout
in a promise chain: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.
Fake timers in Jest does not fake promises (yet: #6876), however - as you discovered - if you use a polyfill for
Promise
that uses eithersetImmediate
orprocess.nextTick
as its implementation, itāll work. I think this is working as intended (for now)?