-
-
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
Add promise support to all the matchers #3068
Conversation
packages/jest-matchers/src/index.js
Outdated
Object.keys(allMatchers).forEach(name => { | ||
expectation[name] = | ||
makeThrowingMatcher(allMatchers[name], false, actual); | ||
expectation.not[name] = | ||
makeThrowingMatcher(allMatchers[name], true, actual); | ||
}); | ||
|
||
const resolveExpectation = {not: {}}; | ||
Object.keys(allMatchers).forEach(name => { |
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.
it seems quite repetitive to receive all the keys every time. Can we avoid it and only iterate over them once?
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.
Good point! I fixed this:)
This is really super awesome. I didn't expect that we could find such an elegant solution not just for We should iterate on this and ship it. One thing I noticed is that this implementation does more work than what we do currently. Could we potentially turn Overall I wish there was an easier way to support this API. I'd love to be able to simply do For Before we go further into this, here are some proposals of what the API could look like:
What do you think? |
@excitement-engineer Thank you very much for your work :) Maybe @cpojer As said in my proposal, I find |
I think when I'm reading it, |
I disagree, everywhere full infinitives are used instead of conjugated at the third person, IMHO, it should stay the same. And |
Hm, unsure about |
I am glad that you guys like the initial proposal!
Good point! We are effectively loading in all the matchers 3 times on every expect call, I agree that this can be optimised further;) Thanks for the tip! I like the idea of adding a lazy getter here, keeps it as simple as possible.
Good point! We could ship without this API. If there are people with a serious use case for simply checking whether a Promise resolves/rejects then we can consider adding it again, but the simpler jest remains the better of course!
I think there may be a potential issue with
Secondly, I don't think it's possible to add some lazy getter in the API @julien-f Thanks again for your gist, it served as inspiration for this PR, awesome work!
I agree with @cpojer here, (await expect(Promise.resolve(4)).toResolve()).toBe(4); is harder to use and less intuitive than: await expect(Promise.resolve(4)).resolves.toBe(4); |
@excitement-engineer Of course the second example is much harder than the first one, but what I suggest is simply to have the same behavior that you implemented but instead of creating resolve matchers in And to avoid recreating those matchers, we would simply need to create them once and for all on a prototype and just create a new instance when necessary: return Object.create(asyncMatchers, { actual: { value: actual } }) @cpojer I'm not a native English speaker but I think “expect something resolves to be” is incorrect unlike “expect something to resolve to be”. |
for perfect readability, I'd go with: // expectation can fail due to 2 different reasons
// promise was not resolved
// promise was not resolved with expected value
await expect(Promise.resolve(4)).toBeResolvedWith(4)
// same logic as above applies
await expect(Promise.reject(4)).toBeRejectedWith(4) But I agree with @cpojer:
My proposal increases the API surface, since separate matchers are required for both assertions and negations |
I'd say there are two things we try to solve here, as @excitement-engineer notes:
Why not to separate it? For the first, we can have simple matcher: await expect(Promise.resolve(4)).toBeResolved()
await expect(Promise.reject(4)).toBeRejected() For the latter, there can be an aforementioned lazy getter which just unwraps the value or reason. (IMHO I like await expect(Promise.resolve(4)).whenResolved.toBe(4)
await expect(Promise.reject(4)).whenRejected.toBe(4) Also, I would be inclined to make the test fail when the getter is not called. But the opposite could make sense, too. // fails (?)
await expect(Promise.reject()).whenResolved.toBe(4) Then the // good
await expect(Promise.resolve(4)).whenResolved.not.toBe(4)
// bad
await expect(Promise.resolve(4)).not.whenResolved.toBe(4) If we used await expect(Promise.resolve(4)).whenResolved.toMatchSnapshot() |
A small note that might be taken into account about naming: There is a difference between ‘resolved’ and ‘fulfilled’ even though they are used interchangeably in everyday talk. Cf. https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md |
Just a devil's advocate-ish thought (and I already probably know the answer, but I figure I'd post it anyway, hehe). Is there a huge gain to chaining promises into assertions in a world where you can just await the promise and run expectations on the result on a separate line? Especially since even with this API you have to await the promise anyway? Is the 1 line save that big? |
@Phoenixmatrix the real gain is for rejection :) (#1377 (comment)) |
fair enough :) |
How about the word yields for both? expect(assertion).yields.{suffix}? |
When it comes to english I thnk that the If I put it in a sentence it seems fine to me
I actually implemented this initially! I was able to write the following code just like your proposal: await expect(promise).toResolve().toBe(); However, I then tried to run the following code and found that it didn't work: await expect(promise).toResolve() When we try to make This is why I decided to forego this API and went with Thanks for your comment! you highlighted the issue we are trying to solve really well:)
Unfortunately such an API would not allow us to distinguish between promises that reject and resolve! |
Thanks everyone for your input. @Phoenixmatrix you are actually making a good point. What is the incremental value that is added when doing something with one of the more complex matchers. For example returning a mock function from a Promise and calling await expect(Promise.resolve(jest.fn())).resolves.toBeCalledWith('foo')`
// vs
const result = await Promise.resolve(jest.fn());
expect(result).toBeCalledWith(); And the answer is probably: not that much. However, as julien-f pointed out, this is much more useful when something is supposed to throw. If we are adding helpers to make it easier to check whether promises fail it doesn't seem like it increases the API surface too much to add checks for proper resolution. @excitement-engineer I still prefer Your argument about Please make the CI pass, too :) (don't worry in case you see a segfault, that's a node issue, but everything else should be green). As a follow-up PR, it would be awesome to add documentation for this feature to the Expect API docs page. What do you think? |
What about: expect.resolution(Promise.resolve(5)).toBe(5);
expect.rejection(Promise.reject(4)).toBe(4);
expect.fulfillment(Promise.resolve(3)).toBe(3); // for either resolved or rejected This feels like more natural English than "expect resolves to be", to me. |
Something I do quite often is checking that the resolved promise is not null for example. It would be nice to include the negation statement for these use cases i.e. it("does something", async () => {
await expect(Promise.resolve(jest.fn())).resolves.not.toBeCalled()
});
it("successfully calls login API", async () => {
await expect(loginAPICall).resolves.not.toHaveProperty("auth_error");
}); However, in general I think that the use cases for Do you agree with my reasoning @cpojer? I will update the PR this weekend, introduce the lazy getter and make sure that the CI passes. Thanks a lot everyone for your feedback and help:) |
Is ‘resolved’ the word we want to use? I know I posted this here already but I feel it was ignored. This one is mainly my conscience.
~ Promise on MDN (definition of settled and resolved taken from the note) cf. Promise Objects in ECMAScript 2015 Language Specification |
@robinpokorny I think this comes mostly from the executor of the Promise function having arguments commonly called "resolve" and "reject". I think it makes sense to stick with what people use colloquially even if it isn't 100 % technically correct. |
IMHO, after thinking I think it would be best to use fulfilled instead of resolved (like Bluebird), because resolved meaning is a bit complicated (jamiebuilds/proposal-promise-prototype-inspect#1 (comment)) and might be confusing for some people. |
@cpojer, in the executor the naming is correct. ‘Fullfill’ is used e.g. in description of Promise.prototype.then: As said, I just want this matter to be discussed. Of course, an alias could solve it later. |
I did some investigation here and implemented the lazy getter to compare it to the current implementation. I implemented the lazy getter as follows: const expect: Expect = (actual: any): ExpectationObject => {
const allMatchers = global[GLOBAL_STATE].matchers;
const expectation = {
not: {},
rejects: {not: {}},
resolves: {not: {}},
};
Object.keys(allMatchers).forEach(name => {
expectation[name] =
makeThrowingMatcher(allMatchers[name], false, actual);
expectation.not[name] =
makeThrowingMatcher(allMatchers[name], true, actual);
});
Object.defineProperties(expectation, {
rejects: ({
get: () => {
const rejectExpectation = {
not: {},
};
Object.keys(allMatchers).forEach(name => {
rejectExpectation[name] =
makeRejectMatcher(name, allMatchers[name], false, actual);
rejectExpectation.not[name] =
makeRejectMatcher(name, allMatchers[name], true, actual);
});
return rejectExpectation;
},
}: Object),
resolves: ({
get: () => {
const resolveExpectation = {
not: {},
};
Object.keys(allMatchers).forEach(name => {
resolveExpectation[name] =
makeResolveMatcher(name, allMatchers[name], false, actual);
resolveExpectation.not[name] =
makeResolveMatcher(name, allMatchers[name], true, actual);
});
return resolveExpectation;
},
}: Object),
});
return expectation;
}; In order to check whether it was actually more efficient I ran the following test using both the current and "lazy" implementation: describe('performance', () => {
beforeAll(() => {
console.time('perf');
});
afterAll(() => {
console.timeEnd('perf');
});
for (let i = 0; i < 10000; i++) {
it('tests', () => {
jestExpect(4).toBe(4);
jestExpect(6).toBeGreaterThan(5);
jestExpect({bla: 5}).toEqual({bla: 5});
});
}
}); These were the results that I got:
The first thing to note is that the added runtime of adding the It turns out that my "lazy" implementation is actually slower! Anyone notices a mistake in my implementation of the lazy getter or has some way of improving perf? I also ran into an unexpected result when running the same tests in an asynchronous "it" function: describe('async performance', () => {
beforeAll(() => {
console.time('perf');
});
afterAll(() => {
console.timeEnd('perf');
});
for (let i = 0; i < 10000; i++) {
it('tests', async () => {
jestExpect(4).toBe(4);
jestExpect(6).toBeGreaterThan(5);
jestExpect({bla: 5}).toEqual({bla: 5});
});
}
}); These were the results for the async test:
This change cut the execution time down by more than 50%! Just an unexpected result that I thought I'd share;) |
I don't really understand why circleci is not running. The terminal output contains an error that I don't understand: Can somebody explain how to fix this:) |
@cpojer @julien-f @robinpokorny
I agree with this! I think it makes a more sense to follow the promise API, which contains the words |
packages/jest-matchers/src/index.js
Outdated
` ${utils.printReceived(result)}` | ||
); | ||
} | ||
return makeThrowingMatcher(matcher, isNot, result)(...args); |
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 guess this is some bug with transform-es2015-destructuring
and it doesn't transform some spread operators. You can use .apply(args)
instead.
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.
Thanks for the help! I have updated the PR:) Let's hope CI runs now
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.
Is there an issue open for this in babel? We should add it if hasn't been done already.
I had to update one of the snapshots in an integration test. Can somebody review this change please? |
The CI is passing! Thanks for your help @thymikee:) |
Ok, let's add this to Jest. Everyone, thanks for weighing in here. We are going with the original proposal but I'm looking forward to pointing people to this PR when they ask about this API and the decisions we made. @excitement-engineer thanks for building this. Would you mind sending a PR to add all of this to the docs? |
Thanks a lot everyone! No problem @cpojer! Expect a PR from me this week:) |
This is a great feature indeed. The only and very confusing part is the comment in the doc. Would love to use this new API changes in my tests but I don't see any hints about Jest version 20, although the docs are talking about it already. Anyone who can help? |
Same issue (feature was in docs before release with no version hint) occurred in #3188; it should be easy to fix, though personally I feel like a change in process would be a better way to address this. |
Could we please not use getters? |
@ljharb This PR has been merged. If you want to improve/change it, I guess it would be best to create a new PR with proposed code changes. |
@robinpokorny I was asked for a suggestion here. I'm afraid I don't have the time to make a PR, but hopefully that won't stop Jest collaborators from improving the code in this PR prior to a release. |
@ljharb, I see 😉 Do I understand it right that you propose to use the following API?
|
@robinpokorny yes, if you don't just want to do |
@ljharb The real benefits of changes in this PR are vastly about rejections, as mentioned here: #3068 (comment).
|
Fair enough :-) then yes, |
@ljharb would you mind explaining your argument a bit better? You said that function calls are:
than the current implementation. We aren't using getters/setters, we only benchmarked it, so that falls away. I assume that "cleaner" and "more readable" is both the same thing and is very subjective and your last argument is that it is easier to lint against, which I don't quite understand given that we are talking about a slight AST difference between the two. As I see it, the added parenthesis serve no value here. Can you elaborate? |
Beside discussing the syntax (and I favor the property syntax btw.): any more info about the upcoming release date for v20? The current documentation is a little misleading. |
@cpojer I assumed based on the syntax that it was using getters (since everything in mocha/chai that looks like that does) - I do now see in this PR that you're creating an object "resolves" and "rejects" with all possible matchers on it, so my concerns indeed don't apply: "readable" is highly subjective, so I'll just withdraw that one rather than have a style debate here; "easier to lint against" applies to arbitrary getters but not to property chains. Making it a function would avoid needing to create these objects in advance; and would allow you to avoid someone doing |
* Adds promise support to all the matchers * avoid duplicate iteration over all the matchers * Fixed bug in spread operator by replacing it with an apply statement * Updated snapshots to new format.
What about:
It does not look very intuitive. |
@drpicox It seems you did not return the promise or the assertion with |
Thanks!
I do not know how I missed it before. Thanks! I just got confused thinking that expect().resolves would talk with the test itself and make it wait until having the result. May be a warning or similar would be nice. |
* Adds promise support to all the matchers * avoid duplicate iteration over all the matchers * Fixed bug in spread operator by replacing it with an apply statement * Updated snapshots to new format.
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. |
This PR addresses issue #1377 and adds promise support to all the existing matchers in jest.
This allows us to run assertions on promises by adding the keywords
resolves
andrejects
after the expect statement as follows:It can also easily be used in combination with
async/await
:I would love to hear some feedback on this API!
Other API Design Choices
@julien-f I also considered the proposal you made in your gist. You proposed the following API:
As far as I can tell the API in your proposal doesn't work unfortunately, we must first
await
for thetoResolve()
before we can run assertions on it. When I tried to implement it I found that the API would look as follows in reality:The above code snippet seemed less intuitive and clean to me. That is why I decided to go with the API I proposed above.
TBD: Add a matcher for checking if promise rejects/resolve:
I think there is a place for a matcher that simply checks whether a promise resolves/rejects similar to the one proposed by @julien-f! This is not something that I implemented in this PR, but just an idea.
Docs
I have not updated the docs yet. First I would like to get some feedback on the API:)