Reputation: 476
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?
Upvotes: 6
Views: 1098
Reputation: 122061
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:
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")
).
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);`
});
...
});
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
Upvotes: 9