4

JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mock...

 1 year ago
source link: https://wanago.io/2022/04/25/javascript-testing-spying-on-functions-resetting-jest-mocks/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks

Testing

April 25, 2022
This entry is part 11 of 11 in the JavaScript testing tutorial

Jest offers a lot of functionalities for mocking functions and spying on them. Unfortunately, there are some pitfalls we need to watch out for. In this article, we explain how spying on functions works. We also explain how and why we should reset our mocks.

Creating mock functions

The most straightforward way of creating a mock function is to use the jest.fn() method.

const mockFunction = jest.fn();

A mock function has a set of useful utilities that can come in handy in our tests. One of them is the mockImplementation function that allows us to define the implementation of our function.

mockFunction.mockImplementation((numberOne: number, numberTwo: number) => {
  return numberOne + numberTwo;
mockFunction(10, 5); // 15

There is also a shorter way to achieve the above by providing a function to the jest.fn() function.

const mockFunction = jest.fn((numberOne: number, numberTwo: number) => {
  return numberOne + numberTwo;

Shorter variants of the mockImplementation function

Particular use-cases of the mockImplementation function can get pretty repetitive. Thankfully, Jest provides us with its shorter variants.

Returning a value

For example, we often want to create a mock function that returns a certain value.

const getUsersMock = jest.fn();
getUsersMock.mockImplementation(() => {
  return [
      name: 'John'

We can simplify the above by using the mockReturnValue function instead.

const getUsersMock = jest.fn();
getUsersMock.mockReturnValue([
    name: 'John'

Returning a resolved promise

We often create mock functions that are supposed to return a promise that resolves to a particular value.

const fetchUsersMock = jest.fn();
fetchUsersMock.mockImplementation(() => {
  return Promise.resolve([
      name: 'John'

We can make the above more readable by using the mockResolvedValue function.

const fetchUsersMock = jest.fn();
fetchUsersMock.mockResolvedValue([
    name: 'John'

Returning a rejected promise

Also, we sometimes want our mock function to return a rejected promise.

const fetchUsersMock = jest.fn();
fetchUsersMock.mockImplementation(() => {
  return Promise.reject('Fetching users failed');

We can make the above example simpler by using the mockRejectedValue function.

const fetchUsersMock = jest.fn();
fetchUsersMock.mockRejectedValue('Fetching users failed');

Spying on a mock function

To check if a function has been called, we can use the toBeCalled function.

const mockFunction = jest.fn((numberOne: number, numberTwo: number) => {
  return numberOne + numberTwo;
mockFunction(10, 5);
describe('The mock function', () => {
  it('should be called', () => {
    expect(mockFunction).toBeCalled();

If we want to be more precise, we can use the toBeCalledTimes and toBeCalledWith functions.

mockFunction(10, 5);
describe('The mock function', () => {
  it('should be called', () => {
    expect(mockFunction).toBeCalledTimes(1);
    expect(mockFunction).toBeCalledWith(10, 5)

If we don’t care about one of the arguments of our function, we can use toBeCalledWith together with expect.anything().

mockFunction(10, 5);
describe('The mock function', () => {
  it('should be called', () => {
    expect(mockFunction).toBeCalledWith(expect.anything(), 5)

expect.anything()  matches anything besides null and undefined.

Getting more details about our mock

We can also access the details of the calls to our function through the mock.calls property.

mockFunction(10, 5);
mockFunction(20, 10);
console.log(mockFunction.mock.calls);
//  [10, 50],
//  [20, 10],

Through the mock.results property, we can get the results of all of the calls made to our mock function.

mockFunction(10, 5);
mockFunction(20, 10);
console.log(mockFunction.mock.results);
//    type: 'return',
//    value: 15
//    type: 'return',
//    value: 30

The type property would contain 'incomplete' if a call started, but didn’t complete yet. The type would contain 'throw' if the call would complete by throwing a value.

The mock.instances property contains all instances of objects created using our mock function and the new keyword.

const mockConstructor = jest.fn();
const firstObject = new mockConstructor();
const secondObject = new mockConstructor();
console.log(mockConstructor.mock.instances[0] === firstObject); // true
console.log(mockConstructor.mock.instances[1] == secondObject); // true

A real-life example of spying

To better grasp the idea of spying, let’s create a straightforward set of functions.

parsePeople.ts
import Person from './person';
import getYoungestPerson from './getYoungestPerson';
import groupPeopleByCountry from './groupPeopleByCountry';
function parsePeople(people?: Person[]) {
  if (!people) {
    return null;
  const peopleGroupedByCountry = groupPeopleByCountry(people);
  const youngestPerson = getYoungestPerson(people);
  return {
    peopleGroupedByCountry,
    youngestPerson,
export default parsePeople;

The above function takes an array of people and performs various operations on the data.

person.ts
interface Person {
  name: string;
  age: number;
  country: string;
export default Person;

One of the operations performed by parsePeople is figuring out the youngest person.

getYoungestPerson.ts
import Person from './person';
function getYoungestPerson(people: Person[]) {
  return people.reduce<Person | null>((youngestPerson, currentPerson) => {
    if (!youngestPerson) {
      return currentPerson;
    if (currentPerson.age < youngestPerson.age) {
      return currentPerson;
    return youngestPerson;
  }, null);
export default getYoungestPerson;

The other operation is grouping the people by country.

groupPeopleByCountry.ts
import Person from './person';
function groupPeopleByCountry(people: Person[]) {
  return people.reduce<Record<string, Person[]>>((result, person) => {
    const currentCountryData = [
      ...(result[person.country] || []),
      person
    return {
      ...result,
      [person.country]: currentCountryData,
  }, {});
export default groupPeopleByCountry;

Writing simple unit tests

Instead of writing unit tests for all of the above functions, we might want to write one integration test for the parsePeople function. In some cases, though, we might want to prefer to write unit tests for each function separately. So let’s start by writing a straightforward test for the groupPeopleByCountry function.

groupPeopleByCountry.test.ts
import Person from './person';
import groupPeopleByCountry from './groupPeopleByCountry';
describe('The groupPeopleByCountry function', () => {
  describe('when provided with an array of people', () => {
    it('should group them by country', () => {
      const people: Person[] = [
          name: 'John',
          age: 30,
          country: 'USA',
          name: 'Jane',
          age: 23,
          country: 'USA',
          name: 'Adam',
          age: 31,
          country: 'Poland',
      const result = groupPeopleByCountry(people);
      expect(result).toEqual({
        USA: [
            name: 'John',
            age: 30,
            country: 'USA',
            name: 'Jane',
            age: 23,
            country: 'USA',
        Poland: [
            name: 'Adam',
            age: 31,
            country: 'Poland',

Let’s also write an elementary unit test for the getYoungestPerson function.

getYoungestPerson.test.ts
import Person from './person';
import getYoungestPerson from './getYoungestPerson';
describe('The getYoungestPerson function', () => {
  describe('when provided with an array of people', () => {
    it('should return the youngest person', () => {
      const people: Person[] = [
          name: 'John',
          age: 30,
          country: 'USA',
          name: 'Jane',
          age: 23,
          country: 'USA',
          name: 'Adam',
          age: 31,
          country: 'Poland',
      const result = getYoungestPerson(people);
      expect(result).toEqual({
        name: 'Jane',
        age: 23,
        country: 'USA',

Mocking certain functions when writing unit tests

Thanks to the above tests, we’ve got a lot of the code covered already. We still need to test the parsePeople function. If we want to take a unit-testing approach, we can start by mocking groupPeopleByCountry and getYoungestPerson functions.

parsePeople.test.ts
import Person from './person';
import getYoungestPerson from './getYoungestPerson';
import groupPeopleByCountry from './groupPeopleByCountry';
jest.mock('./getYoungestPerson', () => ({
  __esModule: true,
  default: jest.fn(),
jest.mock('./groupPeopleByCountry', () => ({
  __esModule: true,
  default: jest.fn(),
describe('The parsePeople function', () => {
  let youngestPerson: Person;
  let peopleGroupedByCountry: Record<string, Person>;
  beforeEach(() => {
    youngestPerson = {
      name: 'Jane',
      age: 23,
      country: 'USA',
    peopleGroupedByCountry = {};
    (getYoungestPerson as jest.Mock).mockReturnValue(youngestPerson);
    (groupPeopleByCountry as jest.Mock).mockReturnValue(peopleGroupedByCountry);
  // ...

If you want to know more about mocking modules, check outJavaScript testing #10. Advanced mocking with Jest and React Testing Library

Thanks to the above, when we call the parsePeople function in our tests, Jest does not call the real groupPeopleByCountry and getYoungestPerson functions. Thanks to that, we can focus on testing the logic contained by the parsePeople function. That has a set of consequences:

  • we can focus on writing tests that check only the parsePeople function,
  • our parsePeople test does not care much about the implementation of the groupPeopleByCountry and getYoungestPerson functions,
  • if either groupPeopleByCountry or getYoungestPerson function breaks, the unit test we wrote for groupPeopleByCountry will still be successful.
parsePeople.test.ts
import parsePeople from './parsePeople';
import Person from './person';
import getYoungestPerson from './getYoungestPerson';
import groupPeopleByCountry from './groupPeopleByCountry';
jest.mock('./getYoungestPerson', () => ({
  __esModule: true,
  default: jest.fn(),
jest.mock('./groupPeopleByCountry', () => ({
  __esModule: true,
  default: jest.fn(),
describe('The parsePeople function', () => {
  let youngestPerson: Person;
  let peopleGroupedByCountry: Record<string, Person>;
  beforeEach(() => {
    youngestPerson = {
      name: 'Jane',
      age: 23,
      country: 'USA',
    peopleGroupedByCountry = {};
    (getYoungestPerson as jest.Mock).mockReturnValue(youngestPerson);
    (groupPeopleByCountry as jest.Mock).mockReturnValue(peopleGroupedByCountry);
  describe('when provided with an array', () => {
    let peopleArray;
    beforeEach(() => {
      peopleArray = [];
    it('should call the getYoungestPerson function', () => {
      parsePeople(peopleArray);
      expect(getYoungestPerson).toBeCalledWith(peopleArray);
    it('should call the groupPeopleByCountry function', () => {
      parsePeople(peopleArray);
      expect(groupPeopleByCountry).toBeCalledWith(peopleArray);
    it('should return the return value of the getYoungestPerson function', () => {
      const result = parsePeople(peopleArray);
      expect(result?.youngestPerson).toEqual(youngestPerson);
    it('should return the return value of the groupPeopleByCountry function', () => {
      const result = parsePeople(peopleArray);
      expect(result?.peopleGroupedByCountry).toEqual(peopleGroupedByCountry);
  describe('when not provided with arguments', () => {
    it('should return null', () => {
      const result = parsePeople();
      expect(result).toBe(null);
    it('should not call the getYoungestPerson function', () => {
      parsePeople();
      expect(getYoungestPerson).not.toBeCalled();
    it('should not call the groupPeopleByCountry function', () => {
      parsePeople();
      expect(groupPeopleByCountry).not.toBeCalled();

The issue of sharing mocks between tests

Unfortunately, some of the above tests are failing.

Screenshot-from-2022-04-23-02-03-43.png

Let’s look at the reason why those tests are not passing.

Error: expect(jest.fn()).not.toBeCalled()

Expected number of calls: 0
Received number of calls: 4

When we look at the implementation of the parsePeople function, we can see that if there are no arguments, the function returns null immediately. This is because Jest shares our mocks between tests in a particular test file by default.

parsePeople.test.ts
describe('when provided with an array', () => {
  let peopleArray;
  beforeEach(() => {
    peopleArray = [];
  it('should call the getYoungestPerson function', () => {
    parsePeople(peopleArray);
    expect(getYoungestPerson).toBeCalledWith(peopleArray);
describe('when not provided with arguments', () => {
  it('should not call the getYoungestPerson function', () => {
    parsePeople();
    expect(getYoungestPerson).not.toBeCalled();

Above, in the first test, we call the parsePeople with an array. Because of that, it calls the getYoungestPerson mock function under the hood.
When we execute the second test right after the first one, the getYoungestPerson mock is already called. Because of that, expect(getYoungestPerson).not.toBeCalled() fails.

Clearing mocks

To deal with the above issue, we can force Jest to clear all the mocks after each test.

parsePeople.test.ts
jest.mock('./getYoungestPerson', () => ({
  __esModule: true,
  default: jest.fn(),
jest.mock('./groupPeopleByCountry', () => ({
  __esModule: true,
  default: jest.fn(),
describe('The parsePeople function', () => {
  afterEach(() => {
    jest.clearAllMocks();
  // ...

When we run jest.clearAllMocks(), Jest clears the information stored in the mock.calls, mock.results, and mock.instances arrays of all mocks. Thanks to doing that after each test, the counter of calls made to our mock function contains zero. Because of that, our tests no longer fail.

If we want to clear just a particular mock, we can call mockFunction.mockClear();

If we want to clear the mocks after every test in all of our test files, we can add "clearMocks": true to our Jest configuration instead of running jest.clearAllMocks().

Resetting mocks

Besides the above, we also have the jest.resetAllMocks() function that is quite similar. Besides clearing the mock.calls, mock.results, and mock.instances properties, it also removes the implementation of our mock.

jest.mock('./getYoungestPerson', () => ({
  __esModule: true,
  default: jest.fn(),
jest.mock('./groupPeopleByCountry', () => ({
  __esModule: true,
  default: jest.fn(),
describe('The parsePeople function', () => {
  afterEach(() => {
    jest.resetAllMocks();
  // ...

If we want to reset only one mock, we can use the mockReset function.

const mockFunction = jest.fn((numberOne: number, numberTwo: number) => {
  return numberOne + numberTwo;
console.log(mockFunction(1, 2)); // 3
console.log(mockFunction.mock.calls.length); // 1
mockFunction.mockReset();
console.log(mockFunction.mock.calls.length); // 0
console.log(mockFunction(1, 2)); // undefined

We might want to reset the mock after each of our tests in all of our test files. In this case, we need to set "clearMocks": true in our Jest configuration.

An important caveat is that Create React App sets clearMocks to true by default.

Summary

In this article, we’ve created mock functions and defined their implementation. We’ve also learned different ways of spying on a mock function. Finally, we wrote a unit test that uses all of the above knowledge to internalize this knowledge better. While doing that, we stumbled upon an issue of sharing mocks. Besides that, we learned how to clear the mocks between tests to prevent our tests from failing.

Series Navigation<< JavaScript testing #10. Advanced mocking with Jest and React Testing Library

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK