I'm writing functional tests using Jest + Testing-Library/React. After days of head scratching, I figured out that when you use .mockResolvedValue(...)
or .mockResolvedValueOnce(...)
the scope of the mocking is not limited to that test...
import React from "react";
import { render, waitForElement } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import myApi from '../myApi';
jest.mock('../myApi'); // this will load __mocks__/myApi.js (see below)
import { wait } from '@testing-library/dom';
import App from "../components/App";
afterEach(() => {
jest.clearAllMocks();
});
describe("App", () => {
test("first test", async () => {
myApi.get.mockResolvedValueOnce('FOO');
// App will call myApi.get() once
const { container, getByText } = render(<App />);
await waitForElement(
() => getByText('FOO')
);
expect(myApi.get).toHaveBeenCalledTimes(1);
// This is going to "leak" into the next test
myApi.get.mockResolvedValueOnce('BAR');
});
test("second test", async () => {
// This is a decoy! The 'BAR' response in the previous test will be returned
myApi.get.mockResolvedValueOnce('FOO');
// App will call myApi.get() once (again)
const { container, getByText } = render(<App />);
// THIS WILL FAIL!
await waitForElement(
() => getByText('FOO')
);
expect(myApi.get).toHaveBeenCalledTimes(1);
});
});
Here's what __mocks__/myApi.js
looks like:
export default {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
I understand what is happening: myApi
is imported into the shared scope of both tests. And this is why the .mockResolvedValue*
applies "across" the tests.
What is the right way to prevent this? Tests should be atomic, and not coupled to one another. If I trigger another get
request in first test
it should not be able to break second test
. That's smelly! But what's the correct pattern? I'm thinking about cloning distinct "copies" of myApi
into the local test scopes... but I worry that will get weird and lead to decreases the confidence of my tests.
I found this question which discusses the same topic, but only explains why this happens rather than discussing the right pattern to avoid it.
package.json
"dependencies": {
"axios": "^0.18.1",
"moment": "^2.24.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "2.1.5",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.3",
"@testing-library/react": "^9.3.2",
"redux-mock-store": "^1.5.3",
"typescript": "^3.7.2"
}
This is how I structure my tests:
beforeAll
block in the beginning of the test suit for
test
/it
block
--verbose
modeexample:
App.spec.js
describe("App", () => {
// jest allows nesting of test suits
// allowing us to have prettier reporting
// and having scoped variables
describe("Api.get returning FOO", () => {
// define variables used in the test suit
let wrapper;
// having the setup here
beforeAll(async () => {
Api.get.mockClear();
Api.get.mockResolvedValue("FOO");
const { container, getByText } = render(<App />);
// expose the container to the scope
wrapper = container;
await waitForElement(() => getByText("FOO"));
});
// write the test cases balow
// each assertion in a separate test block
test("should call the Api once", () => {
expect(Api.get).toHaveBeenCalledOnce();
});
test("should have been called with data", () => {
expect(Api.get).toHaveBeenCalledWith({ x: "y" });
});
test("should match the snapshot", () => {
expect(wrapper).toMatchSnapshot();
});
});
describe("Api.get returning BAR", () => {
// define variables used in the test suit
let wrapper;
beforeAll(async () => {
Api.get.mockClear();
Api.get.mockResolvedValue("BAR");
const { container, getByText } = render(<App />);
// expose the container to the scope
wrapper = container;
await waitForElement(() => getByText("FOO"));
});
test.todo("describe what is supposed to happen with Api.get");
test.todo("describe what is supposed to happen container");
});
});
And back to the question - yes, the mock function will be used through the entire test file, but if you have mockResolvedValueOnce
that has not been consumed (leaked into the next test) either one of the above test cases will fail or you have a poorly written tests.
Edit:
As a thought experiment, can you think of a structure that would "solve" that?
To remove return mocked values and implementations after each test you can use
afterEach(() => {
jest.resetAllMocks()
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With