Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to limit the scope of Jest mocked functions to a single test

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"
  }
like image 489
emersonthis Avatar asked Nov 16 '19 18:11

emersonthis


1 Answers

This is how I structure my tests:

  • Having a beforeAll block in the beginning of the test suit for
    • setting up the mocks
    • clearing the mock functions that are about to be tested
    • rendering the components
    • calling the functions
  • Writing separate test cases each described in own test/it block
    • allowing each to have a better,separate description
    • providing better visibility on what has failed in --verbose mode

example:

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()
});
like image 197
Teneff Avatar answered Sep 21 '22 13:09

Teneff