jest
jest copied to clipboard
feat: implement `mockRestoreInitialImplementation` and `restoreAllInitialMockImplementations`
Summary
This is an attempt to cover a commen use case (for me) that I think needs improvement:
- There's a bunch of tests in one file
- Most tests use the same set of mock implementations
- One of the tests needs a custom mock implementation
I would currently solve this in one of two ways:
- Use
mockImplementationOnceand cross my fingers that the mock is called exactly once - Use a
beforeEachthat overwrite each implementation one by one. This takes up a lot of space and doesn't work very well with manual mocks.
This PR causes each mock to remember it's initial implementation, and provides a way to restore it, so that:
- Initial mock implementations can be used as the "default" implementations throughout a test file.
- Individual tests can override one or more implementations if necessary.
- The initial implementation can be restored by either:
- Restoring each individual mock directly with
mock.mockRestoreInitialImplementation()*. - Restoring all mocks with
jest.restoreAllInitialMockImplementations()* (possibly in a beforeEach/afterEach hook)
- Restoring each individual mock directly with
* I'm not 100% happy about these long method names, I would prefer something shorter but still precise.
~~I don't know for sure if the current commit is enough for jest.mock (and manual mocks) to work, if not, point me in the right direction and I'll do my best to make it work.~~
Update: This works with jest.mock, and also with manual mocks if they are jest functions - which makes a lot of sense since you would otherwise not be able to override one for a single spec.
I'll be happy to add documentation, additional tests etc. should you decide to like the idea.
Test plan
I added a tests for the two added methods mockRestoreInitialImplementation and restoreAllInitialMockImplementations.
Hi jeppester! 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 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 to sign the corporate CLA.
If you have received this in error or have any questions, please contact us at [email protected]. Thanks!
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!
Hi @jeppester, thanks a lot for the PR! I've also come across this case a lot. What I've usually done is to move the initial implementation from the mock declaration into a beforeEach hook as you mentioned.
Pro:
- Makes sure the test cases all start with the same mocking behavior, no test case can forget to
mockRestoreInitialImplementationCon: - Slightly more verbose
- Does not work for mocks coming from
__mocks__
Regarding the pro: I would very much like to preserve that and not encourage a structure where it is easy for one test case to mess everything up. However it's not a deal breaker for me if there are significant upsides to the new approach.
Regarding the second con: Closing this gap might be a significant upside of mockRestoreInitialImplementation. However, I'm wondering how often it would actually be needed, because defining mock implementation in __mocks__ is usually something you do to avoid repeating yourself everywhere for mocks that will always behave the same - if a mock is supposed to have different behaviors it'll likely not be extracted into __mocks__.
Thoughts @SimenB @thymikee @jeppester?
Codecov Report
Merging #9270 into master will increase coverage by
0.05%. The diff coverage is80%.
@@ Coverage Diff @@
## master #9270 +/- ##
==========================================
+ Coverage 64.93% 64.99% +0.05%
==========================================
Files 278 278
Lines 11905 11895 -10
Branches 2935 2927 -8
==========================================
Hits 7731 7731
+ Misses 3544 3535 -9
+ Partials 630 629 -1
| Impacted Files | Coverage Δ | |
|---|---|---|
| packages/jest-mock/src/index.ts | 77.68% <100%> (+0.59%) |
:arrow_up: |
| packages/jest-runtime/src/index.ts | 66.91% <25%> (-0.43%) |
:arrow_down: |
| packages/jest-environment-node/src/index.ts | 50% <0%> (-1.17%) |
:arrow_down: |
| packages/jest-snapshot/src/utils.ts | 95.29% <0%> (-0.32%) |
:arrow_down: |
| packages/expect/src/index.ts | 84.92% <0%> (-0.24%) |
:arrow_down: |
| ...ackages/jest-jasmine2/src/assertionErrorMessage.ts | 0% <0%> (ø) |
:arrow_up: |
| packages/jest-snapshot/src/State.ts | 0% <0%> (ø) |
:arrow_up: |
| packages/jest-circus/src/formatNodeAssertErrors.ts | 12.96% <0%> (+1.09%) |
:arrow_up: |
| packages/jest-snapshot/src/inline_snapshots.ts | 81.81% <0%> (+1.42%) |
:arrow_up: |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact),ø = not affected,? = missing dataPowered by Codecov. Last update 012fc8b...3050885. Read the comment docs.
Hi @jeysal.
Thank you for the quick feedback!
After having tested the code with a manual mock, I'm not sure that there's a gap for close there. It appears to be working just fine - If you have some specific issue in your mind, please let me know, and I'll add a test for that.
Here's how I've tested it (added to transform/multipleTransformers - since I wanted a setup with react):
In src/Test.js:
import React from 'react';
export default () => <div>Real</div>;
In src/__mocks__/Test.js:
import React from 'react';
export default jest.fn(() => <div>Mock!</div>);
In __tests__/manual-mocks.test.js:
jest.mock('../src/Test');
import Test from '../src/Test';
describe('mockRestoreInitialImplementation', () => {
beforeEach(() => {
Test.mockRestoreInitialImplementation();
});
it('allows a test to override the manual mock', () => {
Test.mockImplementation(() => <span>Changed for a single spec!</span>);
const tree = renderer.create(<Test />).toJSON();
expect(tree.children).toEqual(['Changed for a single spec!']);
});
it('allows a following test to still have the manual mock', () => {
const tree = renderer.create(<Test />).toJSON();
expect(tree.children).toEqual(['Mock!']);
});
});
transform/multipleTransformers is obviously not the right place for this test, but I'm not entirely sure where to put it, since there are currently no folders for manual-mocks in the e2e-folder.
Should I add a new e2e test folder or is there already a fitting place for such a test?
Sorry if my message was not ideally structured, "Pro" and "Con" referred to the "what I've usually done" here
Sorry if my message was not ideally structured, "Pro" and "Con" referred to the "what I've usually done" here
Now I understand your feedback much better :smile: - thank you for clarifying.
The pro you've mentioned is definitely a good thing, I do however think it's just as good to have a beforeEach with restoreAllInitialMockImplementations. And the latter will additionally save you many lines.
Also I think a lot of people currently start out with a bunch of mocks using jest.mock('path/to/module', () => jest.fn(someImplementation))).
If you start out that way, and you at some point discover that someModule.mockImplementation is permanent, then I don't think it's more obvious to move all implementations to a beforeEach, than it is to add a beforeEach with restoreAllInitialMockImplementations, especially not if you've already been using clearAllMocks/resetAllMocks/restoreAllMocks.
At least it wasn't obvious to me, and my search for a way to rewind my test-local mock changes is what eventually lead to this PR.
In regards to manual mocks, I haven't used them enough to make a good judgement on how often you would need this feature.
I do however think it's really nice if this feature will prevent manual mocks from having an annoying downside that you might not discover until you have a test where it makes a lot of sense to replace a mock implementation.
I think you've convinced me, especially with the starting out and later discovering you need to restructure thing that I can very much relate to :smile:
To mitigate the potential problem of forgetting to restore the initial impl at the end of a test, maybe we can just write docs that encourage a beforeEach pattern not susceptible to this.
Would be interested what other maintainers think about the API in general before getting into detail review @SimenB @thymikee
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 has been waiting for feedback for soon 3 years. I believe that the issue I was trying to solve is still in jest, I don't think this PR is less relevant today that when it was made.
It would be nice if someone would finally have a look at it.
What are you thoughts @SimenB and @thymikee? (If you are still maintainers)
I'm a bit reluctant to add more APIs here as I don't see a big advantage to it, and our reset, clear and restore APIs already confuse people (including myself). Specifically the one "current way" you point out is what I think the correct solution to your use case is:
- Use
mockImplementationOnceand cross my fingers that the mock is called exactly once
You don't have to cross your fingers, you can just use expect(myMock).toHaveBeenCalledTimes(1) (or however many times you call mockImplementationOnce as they stack until exhausted) at the end of your test.
our
reset,clearandrestoreAPIs already confuse people (including myself)
I'm absolutely with you here. It's the one part of jest that I just can't get right 100% of the time.
I can see why those names where chosen -for simplicity, but more verbose names would help a lot, like for instance resetCalls and clearImplementation.
For that reason I was never 100% happy about my own suggestions here, as they might at another layer of confusion.
they stack until exhausted) at the end of your test.
While this is useful it adds a layer of often unnecessary complexity when testing something that might call the mock a number of times, especially if the number of times is hardly relevant to the test. That the method has built-in stacking is also counterintuitive in my opinion.
I'm now thinking that we could instead have an API on mocks to set af specific implementation, then run a callback, and finally reset the mock to whatever it was.
Something like
mock.withImplementation(implementation, () =>
{
Logic using the temporary implementation
})
I'm not entirely sure that this idea i perfect (for instance how it would fare with multiple mocks -maybe well with decorators?). An API like that would however be very intuitive IMO, and not add to the reset/clear/restore confusion at all.
What are your thoughts?
It'd have to be async I guess, but other than that I think I like it! As you say it'd probably not work too well with multiple mocks, but then again, you can probably nest them within each other?
It'd have to be async I guess, but other than that I think I like it! As you say it'd probably not work too well with multiple mocks, but then again, you can probably nest them within each other?
It's the nesting that might become a little bit too much indenting, but I'll give the idea a go and make a new PR. I would prefer to keep this open until I have another one ready - hopefully already some time this week.
@SimenB
I've created a new PR for withImplementation https://github.com/facebook/jest/pull/13281
I'll close this PR as I believe withImplementation solves same issue as this PR, but (in most cases) in a better way.
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.