6

I am confused about the below behaviour of jest.fn() when run from a clean CRA project created using npx create-react-app jest-fn-behaviour.

Example:

describe("jest.fn behaviour", () => {
    
    const getFunc = async () => {
        return new Promise((res) => {
            setTimeout(() => {
                res("some-response");
            }, 500)
        });;
    }

    const getFuncOuterMock = jest.fn(getFunc);


    test("works fine", async () => {

        const getFuncInnerMock = jest.fn(getFunc);
        const result = await getFuncInnerMock();
        expect(result).toBe("some-response"); // passes
    })


    test("does not work", async () => {

        const result = await getFuncOuterMock();
        expect(result).toBe("some-response"); // fails - Received: undefined
    })

});

The above test will work as expected in a clean JavaScript project but not in a CRA project.

Can someone please explain why the second test fails? It appears to me that when mocking an async function jest.fn() will not work as expected when called within a non-async function (e.g. describe above). It will work only when called within an async function (test above). But why would CRA alter the behaviour in such a way?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
trajan
  • 416
  • 1
  • 4
  • 10
  • It working well on my machine. – hoangdv Apr 07 '21 at 12:40
  • @hoangdv interesting. What if you added `setTimeout()` inside the promise (I've updated the question) just to make sure it's not a race condition problem? – trajan Apr 07 '21 at 13:17
  • `getTodosOuterMock` would potentially have shared state between the tests in that describe block, and is created at test discovery not test execution time; maybe that aspect went missing when you tried to cut this down? – jonrsharpe Apr 07 '21 at 13:57
  • @jonrsharpe I accept this probably isn't a realistic example. However, if I run these tests separately (comment each one in turn) I get the same result. I'm just scratching my head trying to understand why the `async - await` doesn't work for `getTodosOuterMock` function – trajan Apr 07 '21 at 14:16
  • They both pass for me, too, so there's not a lot I can say about that. Please give a [mre]. – jonrsharpe Apr 07 '21 at 14:18
  • @jonrsharpe appreciate you looking into this. I have simplified the example above. The second test always fails on my machine. – trajan Apr 07 '21 at 14:35
  • 1
    That still passes for me, as I'd expect. Note that you probably don't actually _want_ a 500ms delay in a unit test, look into Jest's "fake timers" if you're testing something that's actually using setTimeout. – jonrsharpe Apr 07 '21 at 14:41
  • @jonrsharpe I've only now realised that to reproduce the failing test you need to run it from a React project created using `npx create-react-app`. So it seems this issue is somehow caused by one of its dependencies. Apologies for not checking this sooner. – trajan Apr 08 '21 at 06:45
  • @trajan yep, that blew the case wide open! – jonrsharpe Apr 08 '21 at 07:16

1 Answers1

8

The reason for this is, as I mentioned in another answer, that CRA's default Jest setup includes the following line:

    resetMocks: true,

Per the Jest docs, that means (emphasis mine):

Automatically reset mock state before every test. Equivalent to calling jest.resetAllMocks() before each test. This will lead to any mocks having their fake implementations removed but does not restore their initial implementation.

As I pointed out in the comments, your mock is created at test discovery time, when Jest is locating all of the specs and calling the describe (but not it/test) callbacks, not at execution time, when it calls the spec callbacks. Therefore its mock implementation is pointless, as it's cleared before any test gets to run.

Instead, you have three options:

  1. As you've seen, creating the mock inside the test itself works. Reconfiguring an existing mock inside the test would also work, e.g. getFuncOuterMock.mockImplementation(getFunc) (or just getFuncOuterMock.mockResolvedValue("some-response")).

  2. You could move the mock creation and/or configuration into a beforeEach callback; these are executed after all the mocks get reset:

    describe("jest.fn behaviour", () => {
      let getFuncOuterMock;
      // or `const getFuncOuterMock = jest.fn();`
    
      beforeEach(() => {
        getFuncOuterMock = jest.fn(getFunc);
        // or `getFuncOuterMock.mockImplementation(getFunc);`
      });
    
      ...
    });
    
  3. resetMocks is one of CRA's supported keys for overriding Jest configuration, so you could add:

      "jest": {
        "resetMocks": false
      },
    

    into your package.json.

    However, note that this can lead to false positive tests where you expect(someMock).toHaveBeenCalledWith(some, args) and it passes due to an interaction with the mock in a different test. If you're going to disable the automatic resetting, you should also change the implementation to create the mock in beforeEach (i.e. the let getFuncOuterMock; example in option 2) to avoid state leaking between tests.

Note that this is nothing to do with sync vs. async, or anything other than mock lifecycle; you'd see the same behaviour with the following example in a CRA project (or a vanilla JS project with the resetMocks: true Jest configuration):

describe("the problem", () => {
  const mock = jest.fn(() => "foo");

  it("got reset before I was executed", () => {
    expect(mock()).toEqual("foo");
  });
});
  ● the problem › got reset before I was executed

    expect(received).toEqual(expected) // deep equality

    Expected: "foo"
    Received: undefined
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437