Apr 05, 2023

How to Mock Functions, Classes, and Modules With Jest

How to Mock Functions, Classes, and Modules With Jest

A mock is a special type of function that allows to temporarily override the implementation of a single function, class, or module, to give it another behaviour in order to test it in isolation from external dependencies.

It also allows you to track how many times it has been invoked, with what parameters, and what it returned.

Mocking a single function

Let’s consider the following proxy() function.

function proxy(data, callback) {
  return callback(data);
}

module.exports = proxy;

Testing the execution

To asset that the callback() function is actually invoked when the proxy() function is executed, we can:

  1. Create a mock of the callback() function using the jest.fn() function.
  2. Execute the proxy() function using the mockFn mock.
  3. Test if the mock has been called, how many it has been called, and with what parameters using the following self-descriptive matchers.
const proxy = require('./proxy');

test('it should invoke the callback function', () => {
  // (1)
  const mockFn = jest.fn();

  // (2)
  proxy('Hello World', mockFn);

  // (3)
  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledTimes(1);
  expect(mockFn).toHaveBeenCalledWith('Hello World');
});

Overriding the implementation

To override the implementation of a function, and consequently its behaviour, we can define its new implementation within the jest.fn() function itself, and test its result using the toHaveReturnedWith() matcher.

const proxy = require('./proxy');

test('it should return the data length', () => {
  const mockFn = jest.fn(data => data && data.length || 0);

  proxy('Hello World', mockFn);

  expect(mockFn).toHaveReturnedWith(12);
});

Overriding the returned value

To override the return value of a function instead of its implementation, we can use the mockReturnValue() function which will return the same value for each call.

const proxy = require('./proxy');

test('it should return the data length', () => {
  const mockFn = jest.fn().mockReturnValue(20);
  
  proxy('Hello world', mockFn);

  expect(mockFn).toHaveReturnedWith(20);

  proxy('Bonjour le monde', mockFn);

  expect(mockFn).toHaveReturnedWith(20);
});

Or we can use the mockReturnValueOnce() function which will return a specific value for the next call only.

test('it should return different data lengths', () => {
  const mockFn = jest.fn()
    .mockReturnValueOnce(3)
    .mockReturnValueOnce(5);

  proxy('Hello', mockFn);

  expect(mockFn).toHaveReturnedWith(3);

  proxy('Hello', mockFn);

  expect(mockFn).toHaveReturnedWith(5);
});

Mocking a class

Let’s consider the following User class, whose constructor takes as argument a string representing a name, and implements a hello() function that returns a string containing the name property.

class User {
  constructor(name) {
    this.name = name;
  }

  hello() {
    return "Hello " + this.name;
  }
}

module.exports = User;

Mocking an entire class

Since ES6 classes are essentially constructor functions with some syntactic sugar, any mock for an ES6 class must be a function or an actual ES6 class (which is, again, another function), and can therefore be mocked using the jest.fn() function.

To mock an entire class, we can use the jest.mock() function that takes as argument the path of the module we want to create a mock of, and a callback function that returns a mock created with jest.fn().

const MyClass = require('./myclass');

jest.mock('./myclass', () => {
  return jest.fn(() => {
    return {
      // ...
    };
  });
});

As you can see in this example, if we mock the User class the following way and instantiate a new object using the string "Jack", the value contained in the name property will actually be the one defined in the mock and not the one passed to the constructor.

const User = require('./user');

jest.mock('./user', () => {
  return jest.fn(() => {
    return {
      name: 'John',
      hello: jest.fn()
    };
  });
});

test('it should mock the User constructor', () => {
  const user = new User('Jack');

  expect(user.name).toBe('John');
});

Alternatively, we can also mock a class per test — similarly to functions — using the mockImplementationOnce() method as follows.

const User = require('./user');

jest.mock('./user');

test('it should mock the User constructor', () => {
  User.mockImplementationOnce(() => {
    return {
      name: 'John',
      hello: jest.fn()
    };
  });

  const user = new User('Jack');

  expect(user.name).toBe('John');
});

Spying on a class method

To test the behaviour of a class without changing its actual implementation, we can use the jest.spyOn() function that take as argument an object and a method to track the calls of, and returns a mock function.

const User = require('./user');

test('it should return the string "Hello John"', () => {
  const user = new User('John');

  const mock = jest.spyOn(User.prototype, 'hello');

  user.hello();

  expect(mock).toHaveReturnedWith("Hello John");
});

Mocking a class method

To mock a specific class method without altering any other properties, we can use the jest.spyOn() function we've just seen and chain it to the mockImplementationOnce() method.

const User = require('./user');

test('it should return the string "Hello John"', () => {
  const user = new User('John');

  const mock = jest
    .spyOn(User.prototype, 'hello')
    .mockImplementationOnce(() => {
      return "Gutten Tag John";
    });

  user.hello();

  expect(mock).toHaveReturnedWith("Gutten Tag John");
});

Mocking a module

Creating a mock of an entire module is actually similar to creating a mock of a class, as it can be done directly within the callback function of the jest.mock() function.

jest.mock('module', () => {
  return jest.fn(() => {
    return {
      // ...
    };
  });
});

Let’s consider the following module that instantiates a new database handler using the Sequelize class, and performs a database connection attempt using the authenticate() method of the database handler object.

const Sequelize = require('sequelize');

async function connect(env) {
  const db = new Sequelize(env.name, env.user, env.password, {
    host: env.host,
    port: env.port,
    dialect: env.dialect,
    logging: env.logging
  });
  
  await db.authenticate();

  return db;
};

module.exports = connect;

To assert that the connect function invokes the Sequelize class and the authenticate() method—without actually performing a connection attempt—we can mock the Sequelize module the following way:

const Sequelize = require('sequelize');
const connect = require('./connect');

jest.mock('sequelize', () => {
  return jest.fn(() => {
    return {
      authenticate: jest.fn()
    };
  });
});

afterEach(() => jest.clearAllMocks());

const env = {
  name: 'database',
  user: 'john',
  password: 'hello',
  host: '127.0.0.1',
  port: 3306,
  dialect: 'mysql',
  logging: true
};

test('it should invoke the Sequelize constructor', async () => {
  await connect(env);

  expect(Sequelize).toHaveBeenCalledWith(env.name, env.user, env.password, {
    host: env.host,
    port: env.port,
    dialect: env.dialect,
    logging: env.logging
  });
});

test('it should invoke the authenticate handler', async () => {
  const db = await connect(env);

  expect(db.authenticate).toHaveBeenCalledTimes(1);
});

Related posts