jest icon indicating copy to clipboard operation
jest copied to clipboard

feat: implement `mockRestoreInitialImplementation` and `restoreAllInitialMockImplementations`

Open jeppester opened this issue 5 years ago • 15 comments

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.

jeppester avatar Dec 06 '19 12:12 jeppester

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 avatar Dec 06 '19 12:12 facebook-github-bot

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

facebook-github-bot avatar Dec 06 '19 12:12 facebook-github-bot

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?

jeysal avatar Dec 06 '19 14:12 jeysal

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%) :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 data Powered by Codecov. Last update 012fc8b...3050885. Read the comment docs.

codecov-io avatar Dec 08 '19 19:12 codecov-io

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?

jeppester avatar Dec 08 '19 20:12 jeppester

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

jeysal avatar Dec 08 '19 20:12 jeysal

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.

jeppester avatar Dec 08 '19 21:12 jeppester

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

jeysal avatar Dec 08 '19 21:12 jeysal

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[bot] avatar Sep 08 '22 18:09 github-actions[bot]

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)

jeppester avatar Sep 12 '22 06:09 jeppester

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.

SimenB avatar Sep 12 '22 06:09 SimenB

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?

jeppester avatar Sep 12 '22 21:09 jeppester

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?

SimenB avatar Sep 13 '22 08:09 SimenB

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 avatar Sep 14 '22 07:09 jeppester

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

jeppester avatar Sep 19 '22 09:09 jeppester

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[bot] avatar Oct 23 '22 00:10 github-actions[bot]