-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fake promises for fake timers #6876
Conversation
_Promise class mimics es6's Promise class. FakePromises use _Promise to synchronize promise callbacks when `runAllPromises()` is called.
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed. If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks! |
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
@mjesun @cpojer @aaronabramov how does this fit in with the way you mock promises at FB? |
CHANGELOG.md
Outdated
- `[jest-config`] Better error message for a case when a preset module was found, but no `jest-preset.js` or `jest-preset.json` at the root ([#6863](https://github.com/facebook/jest/pull/6863)) | ||
- `[jest-each]` Prevent done callback being supplied to describe ([#6843](https://github.com/facebook/jest/pull/6843)) | ||
- `[jest-config]` Better error message for a case when a preset module was found, but no `jest-preset.js` or `jest-preset.json` at the root ([#6863](https://github.com/facebook/jest/pull/6863)) | ||
- `[jest-util]` Introduce FakePromises to fix micro-task run-order issues when using FakeTimers. ([6876](https://github.com/facebook/jest/pull/6876)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the number should include #
(same with the docs one)
docs/Configuration.md
Outdated
|
||
Default: `real` | ||
|
||
Setting this value to `fake` allows the use of fake promises to replace the `Promise` class introduced in ecma-script2015 specification. Fake promises are useful for synchronizing promises, or for use with jest's fake timers feature. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Jest's"
@SimenB thanks for the review! I took a quick look at “promise/setImmediate”, and if I understand correctly then the promises would still be running in a different order than in the node environment. For example, if you call
then the example test I included in my PR description would fail, because the setImmediate callback would run before the promise callback. Re: async-await Not sure if it works. I think async-await is just syntactic sugar that reuses the Re: No changes I don’t know what this is. Will investigate further tonight. Also, I was thinking of using fake promises by default when using fake timers, but I was thinking maybe there should be a grace period, where users have to explicitly use fake promises. Otherwise, I can change this PR to use fake/real promises whenever fake/real timers are used. |
Oh, and the idea behind fake promises is for them to be used implicitly with fake timers so that jest users don’t get caught off-guard by the run-order discrepancy (i.e. micro/macro-task run-order should be be the same in node environment vs. when using fake timers in jest). |
Codecov Report
@@ Coverage Diff @@
## master #6876 +/- ##
=======================================
Coverage 66.68% 66.68%
=======================================
Files 254 254
Lines 10915 10915
Branches 4 4
=======================================
Hits 7279 7279
Misses 3635 3635
Partials 1 1 Continue to review full report at Codecov.
|
This is pretty much what FB does internally. To give you a broad vision on that:
(Think about overriding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No comments on the code itself (no time currently), just some hand-waving at the API 🙂
docs/JestObjectAPI.md
Outdated
|
||
Exhausts the **micro**-task queue (`process.nextTick` callbacks and promises scheduled via the `Promise` class). | ||
|
||
Uses the provided `runAllTicks` callback to exhaust all ticks that are currently queued. Fake promises should be in use (by calling `jest.useFakePromises` ) before calling this method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
space between ` and )
docs/JestObjectAPI.md
Outdated
|
||
Uses the provided `runAllTicks` callback to exhaust all ticks that are currently queued. Fake promises should be in use (by calling `jest.useFakePromises` ) before calling this method. | ||
|
||
This is useful for synchronously executing scheduled promises and ticks in the order in their natural run order in node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the order in their natural run order in node.
That reads a bit weird, mind rephrasing? Also, "Node.js"
docs/JestObjectAPI.md
Outdated
@@ -344,11 +347,19 @@ module.exports = { | |||
|
|||
Returns the `jest` object for chaining. | |||
|
|||
### `jest.runAllPromises(runAllTicks)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it a bit odd to pass in a runAllTicks
callback, although I agree with trying to avoid a breaking change in runAllTicks
. I don't have a better suggestion at the moment, so just flagging it 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe drop it and document that people should call runAllTicks
themselves?
Potentially runAllTicksAndPromises
Oooor make runAllTicks
run ticks if fake timers and promises if fake promises?
docs/JestObjectAPI.md
Outdated
@@ -404,12 +415,24 @@ Example: | |||
jest.setTimeout(1000); // 1 second | |||
``` | |||
|
|||
### `jest.useFakePromises()` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should note that async-await
(as long as it's not transpiled into generators and normal Promise
with Babel or something like it) will not work
@mjesun thanks! I take that comment to mean you're not against this landing in core then? Would be awesome if you could test it out in the FB test suite once it's ready to land as well 🙂 Might make it a bit easier to land the Lolex change as well at that point |
My only concern in regards to the change is the need to transpile |
I don't think we should automatically compile away async-await. Rather, it should be implemented in |
Also, I wonder what we should do when |
For the short term, I think it's necessary to compile For the long term, these operations should maybe be done using asynchronous hooks. I'm not sure up to which point we have control at performing operations on the same tick via those hooks, but to me it sounds like a good place to start investigating. |
Solution A: always compile Solution B: compile Just to be clear, are either of these the short-term solution you had in mind? |
Yes, solution A describes my short-term idea exactly. 🙂 cc’ing @rubennorte since we had a related conversation offline few days ago. |
BTW, I think it definitely should be opt-in to transpile async-await, especially as we might be double-transpiling and potentially adding quite some overhead since we'll have to transpile I'm not sure how to best alert users to the fact that they need to turn on transpilation of those things, but I can already imagine the huge amount of issues stemming from it |
warn if fake promises are being used without compiling async-await.
I modified fake promises with the recent comments in mind. To use fake promises you would have to set the new jest option |
Sorta related: sinonjs/fake-timers#114 (and #5171) |
So I'm guessing this isn't going to get merged? |
The promises vs |
There is no nice way to make this work with async-await syntax. IMO the best thing we could do would be to make the limitations of fakePromises clear in the documentation. We could also offer a solution in the docs: perform async-to-generator conversion yourself. I can remove the jest option @SimenB do you have any ideas on how to make this feature a good fit for jest? cc: @jsphstls |
This PR is stale because it has been open 1 year with no activity. Remove stale label or comment or this will be closed in 30 days. |
This PR was closed because it has been stalled for 30 days with no activity. Please open a new PR if the issue is still relevant, linking to this one. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
This PR introduces a way to mock promises for use with fake timers. Up to this point, using the fake timers API would change the order in which promises are being run. When calling
jest.runAllTimers()
orjest.runAllImmediates()
the fake timers class would give priority toprocess.nextTick
callbacks, followed bysetImmediate
callbacks, followed by the next elapsed timer/interval. This run-order overlooks promise callbacks, which are considered micro-tasks in the node run-time environment, and are supposed to be run after nextTick callbacks, but before anysetImmediate
,setTimeout
, orsetInterval
callbacks.A related issue has already been raised: #2157
Test plan
Since this PR introduces a class that mocks the
Promise
API introduced in es6 there are tests that make sure each promise function call would result in the expected behaviour. To run these tests, cd to the jest project root and call:yarn test ./packages/jest-util/src/__tests__/fake_promises.test.js
Moreover, this PR required integration between fake promises and fake timers. To test these changes run:
yarn test ./packages/jest-util/src/__tests__/fake_timers.test.js
Usage
The promise mock API interface is largely inspired by that of timer mocks. Before using the promise mock API you should:
Configure the jest object in your
package.json
file by setting the option"compileAsyncToGenerator": true
, or call jest from the command line with the flag--compileAsyncToGenerator
.Call
jest.useFakePromises()
in your tests. Or, to fake all promises in your test, you can configure your jest object in yourpackage.json
file by setting the option"promises": fake
or call jest from the command line with the option--promises=fake
.You can call
jest.useRealPromises()
to use real promises (real promises are used by default). When using fake promises you can make calls to the promise class as you normally would (i.e.Promise.resolve(value)
,Promise.reject(reason)
,[Promise].then(onResolved, onRejected)
,[Promise].catch(onRejected)
,new Promise((resolve, reject) => {...})
). To empty the promise queue you should calljest.runAllPromises()
.To use fake promises with fake timers simply call
jest.useFakeTimers()
andjest.useFakePromises()
either at the top of your test file, in abeforeEach()
/beforeAll()
method's callback, or in your test callback before using promises/timers. Callingjest.runAllTimers()
with fake promises and timers enabled would run all callbacks from calls to nextTick, promises, immediates, timeouts, and intervals in-order as they would in the node environment, but without waiting.Example