How to mock API calls made within a React component being tested with Jest
Asked Answered
K

4

20

I'm trying to mock a fetch() that retrieves data into a component.

I'm using this as a model for mocking my fetches, but I'm having trouble getting it to work.

I'm getting this error when I run my tests: babel-plugin-jest-hoist: The module factory of 'jest.mock()' is not allowed to reference any out-of-scope variables.

Is there a way I can have these functions return mock data instead of actually trying to make real API calls?

Code

utils/getUsers.js

Returns users with roles mapped into each user.

const getUsersWithRoles = rolesList =>
  fetch(`/users`, {
    credentials: "include"
  }).then(response =>
    response.json().then(d => {
      const newUsersWithRoles = d.result.map(user => ({
        ...user,
        roles: rolesList.filter(role => user.roles.indexOf(role.iden) !== -1)
      }));
      return newUsersWithRoles;
    })
  );

component/UserTable.js

const UserTable = () => {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    getTableData();
  }, []);

  const getTableData = () => {
    new Promise((res, rej) => res(getRoles()))
      .then(roles => getUsersWithRoles(roles))
      .then(users => {
        setUsers(users);
      });
  };
  return (...)
};

component/tests/UserTable.test.js

import "jest-dom/extend-expect";
import React from "react";
import { render } from "react-testing-library";
import UserTable from "../UserTable";
import { getRoles as mockGetRoles } from "../utils/roleUtils";
import { getUsersWithRoles as mockGetUsersWithRoles } from "../utils/userUtils";

const users = [
  {
    name: "Benglish",
    iden: "63fea823365f1c81fad234abdf5a1f43",
    roles: ["eaac4d45c3c41f449cf7c94622afacbc"]
  }
];

const roles = [
  {
    iden: "b70e1fa11ae089b74731a628f2a9b126",
    name: "senior dev"
  },
  {
    iden: "eaac4d45c3c41f449cf7c94622afacbc",
    name: "dev"
  }
];

const usersWithRoles = [
  {
    name: "Benglish",
    iden: "63fea823365f1c81fad234abdf5a1f43",
    roles: [
      {
        iden: "eaac4d45c3c41f449cf7c94622afacbc",
        name: "dev"
      }
    ]
  }
];

jest.mock("../utils/userUtils", () => ({
  getUsers: jest.fn(() => Promise.resolve(users))
}));
jest.mock("../utils/roleUtils", () => ({
  getRolesWithUsers: jest.fn(() => Promise.resolve(usersWithRoles)),
  getRoles: jest.fn(() => Promise.resolve(roles))
}));

test("<UserTable/> show users", () => {
  const { queryByText } = render(<UserTable />);

  expect(queryByText("Billy")).toBeTruthy();
});
Kendra answered 12/4, 2019 at 18:58 Comment(0)
C
21

By default jest.mock calls are hoisted by babel-jest...

...this means they run before anything else in your test file, so any variables declared in the test file won't be in scope yet.

That is why the module factory passed to jest.mock can't reference anything outside itself.


One option is to move the data inside the module factory like this:

jest.mock("../utils/userUtils", () => {
  const users = [ /* mock users data */ ];
  return {
    getUsers: jest.fn(() => Promise.resolve(users))
  };
});
jest.mock("../utils/roleUtils", () => {
  const roles = [ /* mock roles data */ ];
  const usersWithRoles = [ /* mock usersWithRoles data */ ];
  return {
    getRolesWithUsers: jest.fn(() => Promise.resolve(usersWithRoles)),
    getRoles: jest.fn(() => Promise.resolve(roles))
  };
});

Another option is to mock the functions using jest.spyOn:

import * as userUtils from '../utils/userUtils';
import * as roleUtils from '../utils/roleUtils';

const users = [ /* mock users data */ ];
const roles = [ /* mock roles data */ ];
const usersWithRoles = [ /* mock usersWithRoles data */ ];

const mockGetUsers = jest.spyOn(userUtils, 'getUsers');
mockGetUsers.mockResolvedValue(users);

const mockGetRolesWithUsers = jest.spyOn(roleUtils, 'getRolesWithUsers');
mockGetRolesWithUsers.mockResolvedValue(usersWithRoles);

const mockGetRoles = jest.spyOn(roleUtils, 'getRoles');
mockGetRoles.mockResolvedValue(roles);

And another option is to auto-mock the modules:

import * as userUtils from '../utils/userUtils';
import * as roleUtils from '../utils/roleUtils';

jest.mock('../utils/userUtils');
jest.mock('../utils/roleUtils');

const users = [ /* mock users data */ ];
const roles = [ /* mock roles data */ ];
const usersWithRoles = [ /* mock usersWithRoles data */ ];

userUtils.getUsers.mockResolvedValue(users);
roleUtils.getRolesWithUsers.mockResolvedValue(usersWithRoles);
roleUtils.getRoles.mockResolvedValue(roles);

...and add the mocked response to the empty mock functions.

Canberra answered 12/4, 2019 at 22:1 Comment(3)
Hmm. This didn't seem to work for me. Is it because I'm trying to mock npm packages? I use an auto-generated TS client to work with one of my APIs, which is consumed in the component under testHelms
@SteveBoniface all of these strategies work for both user modules and Node modules (npm) so it's likely a different issueCanberra
Okay. Well, FWIW, I have couple different things I'm importing from the same module. Both of which are used in the component under test, calling their constructors and methods on those classes. I've made it far enough so that I can return a mockConstructor for these two dependencies, but how can I actually mock the implementations of the functions on these classes?Helms
S
7

Don't mock the tool making API calls; stub the server responses. Here's how I would re-write your test using an HTTP interceptor called nock.

import "jest-dom/extend-expect";
import React from "react";
import { render, waitFor } from "react-testing-library";
import UserTable from "../UserTable";

const users = [
  {
    name: "Benglish",
    iden: "63fea823365f1c81fad234abdf5a1f43",
    roles: ["eaac4d45c3c41f449cf7c94622afacbc"]
  }
];

const roles = [
  {
    iden: "b70e1fa11ae089b74731a628f2a9b126",
    name: "senior dev"
  },
  {
    iden: "eaac4d45c3c41f449cf7c94622afacbc",
    name: "dev"
  }
];

const usersWithRoles = [
  {
    name: "Benglish",
    iden: "63fea823365f1c81fad234abdf5a1f43",
    roles: [
      {
        iden: "eaac4d45c3c41f449cf7c94622afacbc",
        name: "dev"
      }
    ]
  }
];

describe("<UserTable/>", () => {
  it("shows users", async () => { // <-- Async to let nock kick over resolved promise
    nock(`${server}`)
      .get('/users')
      .reply(200, {
        data: users
      })
      .get('/usersWithRoles')
      .reply(200, {
        data: usersWithRoles
      })
      .get('/roles')
      .reply(200, {
        data: roles
      });
    const { queryByText } = render(<UserTable />);

    await waitFor(() => expect(queryByText("Billy")).toBeTruthy()); // <-- Is this supposed to be "Benglish"?
  });
});

Now your test suite is unaware of how you get the data, and you don't have to maintain complicated mocks. Check out a blog post I wrote Testing Components that make API calls for a deeper dive.

Subotica answered 16/2, 2021 at 5:50 Comment(2)
What does ${server} need to be when running tests with Create React App?Ling
@Ling depends. If you're using a SPA with a separate endpoint, it might have a specific name like https://api.yourapp.com. If it's just a local endpoint, maybe it's http://localhost:3000. It really depends on how you're making the fetch call.Subotica
S
3

These days Mock Service Worker looks like a good option, to "mock by intercepting requests on the network level".

Silversmith answered 28/1, 2022 at 5:34 Comment(0)
P
-3

You need to rename the variables used in the scope of the mocks to be prefixed with mock (e.g. mockUsers).

Jest does some hoisting magic to allow you to replace the imported modules with mocks, but does seem to require these special variable name prefixes to do its thing.

Potsdam answered 12/4, 2019 at 20:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.