Quickest and Simplest Way of Mocking Module Dependencies With Jest

Jest offers many features out of the box. One that is very powerful and commonly used in unit tests is the auto mock feature, which is when Jest automatically mocks everything exported by a module that is imported as a dependency by any module we are testing. Due to Jest’s extensive list of features, the auto mock feature can be easily missed—especially because the documentation doesn’t explicitly focus on it (it’s mentioned in the The Jest Object, Mock Function and ES6 Class Mocks sections).

In this article, we’ll cover the simplest and quickest way of mocking any dependency—external or internal—with Jest just by calling jest.mock, without changing the implementation of the modules.

MOCKING AN EXTERNAL DEPENDENCY

Suppose we have these extracted API calls using axios:

// api/users.js
import axios from 'axios';

export const getUsers = async () => {
  const { data } = await axios.get('/api/users');
  return data;
};

export const getUserData = async (id) => {
  const { data } = await axios.get(`/api/user/${id}`);
  return data;
};

If we want to unit test this simple function and don't want to call the API every time the test runs, we can solve this by simply calling jest.mock:

// __tests__/api/users.js
import axios from 'axios';
import { getUsers, getUserData } from '../../api/users';

jest.mock('axios');

const axiosGet = axios.get;

beforeEach(() => {
  axiosGet.mockReset();
});

describe('getUsers', () => {
  it('should call the API and return the data', async () => {
    const usersData = [
      { id: 51, name: 'Allan' },
      { id: 120, name: 'George' },
    ];

    axiosGet.mockResolvedValue({ data: usersData });
    // also could be mockImplementation 
    // or anything that mock functions can do

    const returnedUsersData = await getUsers();

    expect(returnedUsersData).toEqual(usersData);
    // and because axios.get is a mock function, we can assert it too
    expect(axiosGet).toHaveBeenCalledTimes(1);
    expect(axiosGet).toHaveBeenCalledWith('/api/users');
  });
});

describe('getUserData', () => {
  it('should call the API and return the data', async () => {
    const userId = 51;
    const userData = { id: userId, name: 'Allan' };

    axiosGet.mockResolvedValue({ data: userData });

    const returnedUserData = await getUserData(userId);

    expect(returnedUserData).toEqual(userData);
    expect(axiosGet).toHaveBeenCalledTimes(1);
    expect(axiosGet).toHaveBeenCalledWith(`/api/user/${userId}`);
  });
});

We can call jest.mock('axios') after importing axios because Jest will hoist all jest.mock calls to the top of the file. (Reference)

When we call jest.mock('axios'), both the axios module imported in the test and the module imported by users.js will be the mocked version and the same one imported in this test.

We need to reset the axios.get mock before each test because all tests in the file share the same mock function. (In this case, we could achieve the same result with mockClear, but mockReset is safer.)

If we were using TypeScript and we wanted the autocompletion safety for the mock functions, we could write where we have const axiosGet = axios.get:

const axiosGet = axios.get as jest.Mock;

We need to type cast the function because without doing so, TS wouldn't know that axios.get was mocked.

MOCKING AN INTERNAL DEPENDENCY

Suppose we have a module that does more complex data processing:

// utils/getSortedUserData.js
import { getUserData } from '../api/users';

const getSortedUserData = async (id) => {
  const userData = await getUserData(id);

  const contactsSortedByName = [...userData.contacts]
  	.sort((a, b) => {
      ... // implementation details are not relevant to this article
  	});

    ...
  });

  return {
    user: userData,
    contactsSortedByName,
    contactsSortedByAge,
  };
};

export default getSortedUserData;

...and suppose we want to test if we are processing the data correctly. In this case, we could use jest.mock for either axios or getUserData, but for the purpose of having an example of mocking internal modules, our example will mock users.js:

// _tests_/utils/getSortedUserData.js
import { getUserData } from '../../api/users';
import getSortedUserData from '../../utils/getSortedUserData';

// the path of the mocked module should be related to the test file
jest.mock('../../api/users');

describe('getSortedUserData', () => {
  it('should get user data and sort the contacts', async () => {
    const userId = 51;
    const userData = {
      id: userId,
      name: 'Allan',
      // the detailed data are not relevant to this article
      contacts: [...], 
    };
    const expectedSortedUserdata = {
      user: userData,
      contactsSortedByName: [...],
      contactsSortedByAge: [...],
    };

    getUserData.mockResolvedValue(userData);

    const sortedUserData = await getSortedUserData(userId);

    expect(sortedUserData).toEqual(expectedSortedUserdata);

    expect(getUserData).toHaveBeenCalledTimes(1);
    expect(getUserData).toHaveBeenCalledWith(userId);
  });
});

When we mock an internal module, we use the path to the target module related to the test file where we call jest.mock, not the path used by the modules being tested.

Also worth pointing out is that we import anything exported by the mocked module in the same way that it was exported, named exports and/or default export.

CONCLUSION

A simple jest.mock call allows us to intercept any dependency of the modules we are testing, without needing to change anything in terms of implementation. Jest exposes everything exported by the mocked module as mock functions, which allows us to manipulate their implementation as needed via our test suites. It also lets us assert that the modules being tested are using the mocked module properly.

FURTHER READING

jest.mock accepts two more arguments: a module factory, which is a function that returns the mock implementation, and an object that can be used to create virtual mocks—mocks of modules that don’t exist anywhere in the system.

Using the module factory argument usually results in more work because of the differences between CommonJS modules and ES6 modules. These differences need to be taken into consideration.

The second argument can be necessary for some use cases, for example:

  • Jest is not able to auto mock an external module. This could be, for example, because the installed module is only available on a minified build version.
  • There are times when we need to mock part of a module and have the original implementation for some of its exported properties.

For a more in-depth guide for mocking modules with Jest—which covers the use of the second argument—I recommend Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports.

Article originally published on https://rodgobbi.com/.

Share Article
Rodolfo Gobbi

Rodolfo Gobbi

Frontend Developer • I like to code

You might also like...