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

feat: implement mockRestoreInitialImplementation and restoreAllInitialMockImplementations #9270

Closed

Conversation

jeppester
Copy link
Contributor

@jeppester jeppester commented Dec 6, 2019

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 mockImplementationOnce and cross my fingers that the mock is called exactly once
  • Use a beforeEach that 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:

  1. Initial mock implementations can be used as the "default" implementations throughout a test file.
  2. Individual tests can override one or more implementations if necessary.
  3. 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)

* 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.

@facebook-github-bot
Copy link
Contributor

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!

@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@jeysal
Copy link
Contributor

jeysal commented Dec 6, 2019

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 mockRestoreInitialImplementation
    Con:
  • 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?

@jeppester jeppester force-pushed the feature/reset-implementation branch from 4f91e32 to 3050885 Compare December 8, 2019 19:38
@codecov-io
Copy link

Codecov Report

Merging #9270 into master will increase coverage by 0.05%.
The diff coverage is 80%.

Impacted file tree graph

@@            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%) ⬆️
packages/jest-runtime/src/index.ts 66.91% <25%> (-0.43%) ⬇️
packages/jest-environment-node/src/index.ts 50% <0%> (-1.17%) ⬇️
packages/jest-snapshot/src/utils.ts 95.29% <0%> (-0.32%) ⬇️
packages/expect/src/index.ts 84.92% <0%> (-0.24%) ⬇️
...ackages/jest-jasmine2/src/assertionErrorMessage.ts 0% <0%> (ø) ⬆️
packages/jest-snapshot/src/State.ts 0% <0%> (ø) ⬆️
packages/jest-circus/src/formatNodeAssertErrors.ts 12.96% <0%> (+1.09%) ⬆️
packages/jest-snapshot/src/inline_snapshots.ts 81.81% <0%> (+1.42%) ⬆️

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 012fc8b...3050885. Read the comment docs.

@jeppester
Copy link
Contributor Author

jeppester commented Dec 8, 2019

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?

@jeysal
Copy link
Contributor

jeysal commented Dec 8, 2019

Sorry if my message was not ideally structured, "Pro" and "Con" referred to the "what I've usually done" here

@jeppester
Copy link
Contributor Author

jeppester commented Dec 8, 2019

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 😄 - 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.

@jeysal
Copy link
Contributor

jeysal commented Dec 8, 2019

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 😄

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

@github-actions
Copy link

github-actions bot commented Sep 8, 2022

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.

@github-actions github-actions bot added the Stale label Sep 8, 2022
@jeppester
Copy link
Contributor Author

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)

@github-actions github-actions bot removed the Stale label Sep 12, 2022
@SimenB
Copy link
Member

SimenB commented Sep 12, 2022

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 mockImplementationOnce and 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.

@jeppester
Copy link
Contributor Author

jeppester commented Sep 12, 2022

our reset, clear and restore APIs 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?

@SimenB
Copy link
Member

SimenB commented Sep 13, 2022

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?

@jeppester
Copy link
Contributor Author

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.

@jeppester
Copy link
Contributor Author

@SimenB
I've created a new PR for withImplementation #13281

I'll close this PR as I believe withImplementation solves same issue as this PR, but (in most cases) in a better way.

@jeppester jeppester closed this Sep 22, 2022
@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 Oct 23, 2022
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.

5 participants