Skip to content
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 toHaveLastReturned and toHaveNthReturned spy matchers. #6371

Closed
wants to merge 3 commits into from
Closed

Add toHaveLastReturned and toHaveNthReturned spy matchers. #6371

wants to merge 3 commits into from

Conversation

UselessPickles
Copy link
Contributor

Summary

Adds "toHaveLastReturned" and "toHaveNthReturned" spy matchers to round out the collection of "return" spy matchers.

Test plan

Updated existing test cases for "toHaveReturned" to also test the new matchers.

@codecov-io
Copy link

Codecov Report

Merging #6371 into master will increase coverage by 0.05%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #6371      +/-   ##
==========================================
+ Coverage   63.47%   63.53%   +0.05%     
==========================================
  Files         227      227              
  Lines        8677     8721      +44     
  Branches        4        3       -1     
==========================================
+ Hits         5508     5541      +33     
- Misses       3168     3179      +11     
  Partials        1        1
Impacted Files Coverage Δ
packages/jest-jasmine2/src/each.js 66.66% <0%> (-33.34%) ⬇️
packages/jest-jasmine2/src/reporter.js 76.19% <0%> (-0.74%) ⬇️
packages/jest-runner/src/run_test.js 5.08% <0%> (+1.63%) ⬆️
packages/jest-jasmine2/src/jasmine_async.js 12% <0%> (+12%) ⬆️
packages/jest-jasmine2/src/index.js 16.5% <0%> (+16.5%) ⬆️
packages/jest-jasmine2/src/error_on_private.js 26.66% <0%> (+26.66%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update aff9681...d14e498. Read the comment docs.

@UselessPickles
Copy link
Contributor Author

@SimenB @rickhanlonii

@UselessPickles
Copy link
Contributor Author

Looks like I forgot to add unit tests for "toHaveNthReturned" that actually tests the "nth" param part. Do not merge yet.

@rickhanlonii
Copy link
Member

I'm not sure we need these in core, there's no parity with the called matchers and the use cases for when you need lastReturned but not lastReturnedWith are pretty small

@UselessPickles
Copy link
Contributor Author

UselessPickles commented Jun 1, 2018

Requiring parity with called matchers doesn't make sense, because the returned matchers test how the call completed (returned vs threw).

We already have toHaveReturned matcher that tests if any call returned (rather than threw), so why not also have toHaveLastReturned and toHaveNthReturned to test if a particular call returned (rather than threw).

I plan to next work on "throw" matchers (toHaveThrown, toHaveThrownWith, toHaveLastThrown, toHaveLastThrownWith, toHaveNthThrown, toHaveNthThrownWith, toHaveThrownTimes).

That's where there should really be parity: between the "return" and "throw" matchers. It definitely makes sense to have toHaveLastThrown and toHaveNthThrown, so why not the same for "return" matchers?

@rickhanlonii
Copy link
Member

Big +1 on the throw matchers, I think all of the spy matchers should be:

Call Return Throw
toBeCalled toReturn toThrow
toBeCalledTimes toReturnTimes toThrowTimes
toBeCalledWith toReturnWith toThrowWith
nthCalledWith nthReturnedWith nthThrownWith
lastCalledWith lastReturnedWith lastThrownWith

so why not also have toHaveLastReturned

IMO:

  • it creates confusion with toHaveLastReturnedWith
  • has a small set of use cases not covered already
  • symmetry with call matchers seems to be a pretty reasonable heuristic when deciding where to draw the line for what makes it into core vs what doesn't

I'm not sure it makes sense to have toHaveLastThrown or toHaveNthThrown either. How many users will really want to test toHaveLastThrown/Returned but not care about what it threw/returned with?

@UselessPickles
Copy link
Contributor Author

UselessPickles commented Jun 2, 2018

See #6381 for more justification that exact parity between "called" and "returned" does not make sense. A function may have been called, but not yet returned.

it creates confusion with toHaveLastReturnedWith

How is it any different than the difference between toHaveBeenCalled vs toHaveBeenCalledWith, or tohaveReturned vs toHaveReturnedWith? The pattern is consistent: toHaveNthReturned simply tests if the nth call did successfully return, while toHaveNthReturnedWith further tests the value that it returned.

The mismatch between returned vs call matchers for "nth" and "last" variants is because there is no need at all to ever test if the "nth" call or "last" call was called without testing the arguments. That is covered simply by the existing toHaveBeenCalled or toHaveBeenCalledTimes. If the mock "has been called", then by definition the "last call was called". If the mock "has been called N times", then by definition, the "Nth call was called".

The same is not true for "returned", because the existence of a call does not guarantee a return (the call may not have returned yet, or it may have thrown). Testing if a mock has returned at all, or has returned N times also does not guarantee anything about whether the last or Nth call returned.

@rickhanlonii
Copy link
Member

Let's ignore parity with call matchers, it's a minor point and I agree with you that it shouldn't matter 👍

I see your point with called matchers never needing a last/nth without args. In the cases where you don't care about the returned/thrown value, only that it returned/threw anything, what's wrong with doing this:

expect(mock).nthReturnedWith(1, expect.anything());
expect(mock).lastReturnedWith(expect.anything());

expect(mock).nthThrownWith(1, expect.anything());
expect(mock).lastThrownWith(expect.anything());

Typically when there's been proposed matchers for something achievable with existing matchers like this, we recommend the "sugar" matchers be added to jest-extended. For example, I can see just as many use cases for:

  • toHaveFirstReturned
  • toHaveReturnedAtLeast(times)
  • toHaveReturnedAtMost(times)
  • toHaveReturnedWithTimes(arg, times)
  • toHaveFirstThrown
  • toHaveThrownAtLeast(times)
  • toHaveThrownAtMost(times)
  • toHaveThrownWithTimes(arg, times)
  • etc

But we wouldn't add these to core either. We have to draw the line somewhere so we don't bloat core. As a reference, we don't even have some matchers as common as .toBeArray() or .toBeTrue(), so it needs to be a really compelling use case or difficult to do outside of core to make the cut

@UselessPickles
Copy link
Contributor Author

UselessPickles commented Jun 2, 2018

I was not aware of expect.anything(). My persistence has been under the assumption that there's no way to test whether the nth/last call returned "anything" with the existing matchers. With this new knowledge, I agree that it's not worth bloating core with these new matchers.

Do you think it's worth updating the documentation to suggest the use of expect.anything() if you just want to test if the call successfully returned?

Somewhat related: Now that I've introduced the concept of a call that has not yet completed, what do you think about adding "completed" matchers that simply test if the mock has completed (either returned or threw)?

  • toHaveCompleted()
  • toHaveCompletedTimes(count)
  • toHaveNthCompleted(nth)
  • toHaveLastCompleted()

@SimenB
Copy link
Member

SimenB commented Jun 2, 2018

It will always be "completed", won't it? Inside the recursion itself it might not be, but that (shouldn't) be observable from the outside, should it?

EDIT: not to say tracking "completness" is not good, but I think it's quite edge casey matchers. Seems great for jest-extended, though :)

@UselessPickles
Copy link
Contributor Author

Not only recursion, but also nested function calls. It wouldn't be a common case, but could be useful for verifying something like whether or not a callback is called synchronously or asynchronously.

Example:

test("callback is called synchronously", (done) => {
  const spy= jest.fn(doSomethingThenCallbackSynchronously);

  spy(() => {
    // Confirm that the callback is called synchronously
    expect(spy).not.toHaveLastCompleted();
    done();
  });
});

test("callback is called asynchronously", (done) => {
  const spy= jest.fn(doSomethingThenCallbackAsynchronously);

  spy(() => {
    // Confirm that the callback is called asynchronously
    expect(spy).toHaveLastCompleted();
    done();
  });
});

Of course, that's an over-simplified example that could be easily reworked to spy on the callback and test whether the callback has been called immediately after calling the outer function.

I'm trying to imagine if there could be more complex situations where a callback is indirectly/asynchronously passed to some method of a class that for some reason is impractical to call directly in a unit test, but can be spied upon. If so, then testing from within the callback whether the outer spied-upon method has completed or not would be a practical way to confirm whether the callback is called synchronously or asynchronously.

@SimenB
Copy link
Member

SimenB commented Sep 17, 2018

Since we're incredibly bad at following this up, this can make any breaking changes it wants to - the next release of Jest will be a major

@rickhanlonii
Copy link
Member

Still not crazy about adding all of these matchers - @UselessPickles do you feel strongly that they're in core and not jest-extended?

@UselessPickles
Copy link
Contributor Author

I think it would be sufficient to update documentation of nthReturnedWith and lastReturnedWith with a tip about using jest.anything() to simply test that the function call returned (as opposed to threw an error, or not yet completed).

I still think there's some confusion about which matchers should exist for "called" vs "returned"/"thrown"/completed". Why have toReturn() and toReturnWith(expectedValue), but not a corresponding pair of nthReturned() and nthReturnedWith(expectedValue)? If we don't need nthReturned() because it can be achieved with nthReturned(jest.anything()), then we don't need toReturn() because it can be achieved with toReturnWith(jest.anything()).

If/when "throw" matchers are implemented similar to the "return" matchers, then a similar tip on those matchers would be nice too.

The "throw" matchers will be a bit messy because there's already a toThrow matcher, but it's not for function mocks. How to deal with that conflict? Also, naming of "throw" matchers might be weird: toHaveLastThrown(exectedValue) or toHaveLastThrownWith(expectedValue), for example.

@mattphillips
Copy link
Contributor

Hey @UselessPickles thanks a lot for opening this I don't think that we are going to land this in core for now, this would be awesome in jest-extended though could you please open this up over there? 😄

@SimenB
Copy link
Member

SimenB commented Mar 19, 2019

FWIW, I think it makes sense to trim out (move to jest-extended) some of the more specific matchers from Jest as well. IMO we have too many now

@github-actions
Copy link

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.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants