Search code examples
javascriptreactjsjestjsreact-testing-library

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


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();
});

Solution

  • 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.