Better timeout errors through deadline checking
See original GitHub issueš Feature Proposal
-
Expose the computed test timeout to tests, so they can make decisions based on this timeout.
-
Provide a utility to check if the timeout is expired, or nearly expired, so that tests can unwind, and produce a better error.
-
Use this utility wherever possible, such as during
expect
orawait
.
I have a monkey-patching/internals-abusing implementation here: https://github.com/FauxFaux/jest-fixup-timeouts#jest-fixup-timeouts . In this implementation, I have gone with the proposal below. Thereās no need for any of this to be monkey patching, it feels like it would fit quite well as a PR, but itās way easier to prototype and test this way.
-
I propose calling the timeout ā
test.deadline
ā, and it being a unix timestamp (orDate
?). Adeadline
isDate.now() + timeout - fudge
, for somefudge
which allows realistic code to unwind (20ms? 1%?). -
I have added a function named
expect.withinDeadline
which takes aPromise
, and races it againsttest.deadline
, resolving with the result, or rejecting with the result, or rejecting with aTimeoutError
. It would also be great ifawait expect(..).resolves
did this, but I failed to monkey-patch it. -
Thereās an (optional) babel plugin which rewrites
(await foo())
to(await expect.withinDeadline(foo())
so you get this on allawaits
, instead of just the ones covered by expect. This can be applied to tests, or wherever you want.
Motivation
We use jest-circus
essentially as an integration testing framework. Many of our tests fail with ātimeout exceeded, consider increasing the timeoutā, with no indication of what was happening at the time. We are then informed that our test leaked handles (as it is still running). This is a very poor user experience.
Example
Consider a test like:
it('removes existing resources', async () => {
await registerResources('yellow');
await expect(removeResources('yellow')).resolves.toBe('204 gone');
});
We have this fail with:
Error: thrown: "Exceeded timeout of 5000 ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
This gives you no indication of which operation failed; what the test was doing; which bit is causing this flaky behaviour.
With the ha⦠proposal, this will give an error more like:
ā removes existing resources
deadline exceeded (waited here for 4932ms)
2 | it('removes existing resources', async () => {
> 3 | await registerResources('yellow');
| ^
at Object.<anonymous> (test/demo.spec.ts:3:3)
Pitch
Three parts; the first two feel very related to the core; as theyāre a new major part of the āglobalsā (test
/expect
) API.
-
Deadlines: Itās a piece of data we hand to the core platform, but which it then hides from us. We could stash it in an environment variable, or another global, maybe? Feels very much like core to me.
-
expect.withinTimeout
doesnāt need to be core, iftest.deadline
exists. However:
- thereās probably very limited uses of
test.deadline
outside of that utility function (the only one Iāve come up with is to pass it to the RPC client, so it can pass it on to remote services, and they can do something sensible), - and itās not a matcher,
*and extending the
expect
object directly isnāt normal (or possible?), and it doesnāt feel like it should be elsewhere.
- The
babel
plugin is fine as a 3rd party plugin. But, the feature isnāt very useful without it, and the plugin is quite simple, by babel standards; maybe it can just be merged intobabel-jest
?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:5
- Comments:5 (1 by maintainers)
Iām interested in this.
My main problem with Jest 27 has become tests timing out (which may lead to the Jest process never finishing to OS level, but thatās another plot). Had made some designs of my own (no PR) before finding this.
This allows making work-arounds, but doesnāt cure the deeper problems.
This is what Iām mainly after.
One of the related problems is that JavaScript promises are not cancellable - but they can be cancelled āfrom withinā if one is in charge of the original
new Promise
code. For my use cases, having abeforeTimeout
callback would be sufficient. ThePromise
body could get that knowledge, and turn itself to either resolve (sometimes in my code ātimed outā is the expected behaviour) or reject.Why this matters is that it also provides a mechanism to make sure tests donāt leave Promises dangling. At the moment, this happens, and at least for my CI, it means the Jest process does not return to the OS and the CI also times out.
This is where the approaches differ - and where discussion would be welcome. I would not touch
expect
orawait
but handle the issue deeper, at the source of the Promise waited upon itself.My offer: to take part in design/testing of such a mechanism, for making the Jest user experience better.
I think in the end
resolves
/rejects
will not be sufficient for this to make an impact. Because of async-await, I donāt find myself using them a lot anymore, mostly just forrejects
.expect(await findByText('...')).toBeVisible()
is just so much prettier thanawait expect(findByText('...')).resolves.toBeVisible()
. So I think including it just forresolves
/rejects
is almost the same as not including it by default at all and pointing people to a third party add-on, meaning itāll end up with little usage. I think if we are convinced by the solution, think it brings a lot of benefit and has no pitfalls / ugly edge cases, then we want to go all in on it.