Mocking a JavaScript Class with Jest, Two Ways to Make it Easier

updated on 30 November 2022

Jest is the most popular automated testing framework for JavaScript. It can be used both on the front end and back end with Node.js. Jest is a feature-rich, batteries included testing framework. Amongst other valuable features, the ability to mock dependencies out of the box is a useful one.

In this post, you will learn about the need for mocking in unit testing and the difference between dependency injection and mocking. You will also get acquainted with mocking in Jest. Furthermore, you will see an example script with two classes and mock the dependent class to test the other class with a full code example using module factory and Jest SpyOn. Let’s get rolling!

Need of Mocking in Unit Testing

There are multiple types of tests used in software engineering. Some of the popular ones are unit testing, integration testing, end-to-end testing, smoke testing, etc. Among these types, unit testing is one of the fastest and repeatably reliable forms of automated testing. Unit tests are super fast and predictably reliable because they test one particular unit of code in isolation. This unit of code is generally a function or method.

To test a piece of code swiftly and have consistent reliable results all the other dependencies have to be replaced with mocks controlled by the software engineer writing the tests. For example, if you are testing a function that relies on a file in the file system or makes an HTTP call over the network these external resources must be replaced with mocks for the tests to be a unit test. Relying on any external resource can make the test results flaky and hence unreliable.

Any external resource or even code outside the system under test should be mocked out. Replacing any external resource like network calls or file system access not only makes the unit test blazing fast but also removes any chance of the test failing due to external factors out of your control. For instance, if a test is doing an HTTP call to another server and that server is down for 1 hour, your unit tests don’t fail as the unit test verifies that piece of code in isolation sans any dependencies.

To make the unit tests repeatedly reliable and blazing fast any external code and external resources must be replaced with dummy artifacts that are controlled by the software engineer.

Jest also supports mocking in various forms, for the scope of this post you will learn how to mock an ES6 Class with Jest. In the next part, you will find out the difference between dependency injection and mocking.

Dependency Injection vs Mocking

Dependency injection is a pattern where any dependency used in a class is not instantiated within the class, it is injected from an external source into the class. This source knows how to instantiate all required dependencies for a class to function properly. Separating the instantiation of objects from their use stems from the inversion of control concept. Conversely, if the dependencies for a class are initialized within the class, you lose the ability to easily swap them out when doing unit tests.

For example, suppose you want to get the latest tweets of a person from their GitHub username. The tweets retriever class will need two other classes. The first is the GitHub client to pull in the profile of the person and pluck out the Twitter user name. The second is the Twitter client which will talk to the Twitter API to fetch the latest tweets. Both the Github and Twitter client classes can be passed into the tweets retriever class and then be used in the class. 

This eliminates the need for the tweets retriever class on how to create both dependencies and knowing things like the API keys for both services.

It can be visualized as follows:

dependency-injection-visualized-brph2

With the use of Dependency Injection, the Tweets Retriever receives the GitHub Client and the Twitter Client classes in its container. As these objects have been instantiated the Tweets Retriever class does not need any details or configuration to instantiate both of them. The Dependency Injection concept is geared more toward the object-oriented programming paradigm.

Mocking is a technique used for unit testing where any code or non-code dependencies are swapped out with dummy implementations. The software engineer writing the tests has full control over these dummy implementations. These types of dummy objects have other forms too. Some of them are spies, fakes, and stubs. For instance, stubs respond with canned answers to calls when running tests. Spies are special stubs that also record some information based on how they were called.

The main idea here in relation to the Tweets Retriever class is while running the unit tests both the dependencies of GitHub Client and Twitter Client can be swapped on the fly. These swapped mocks will respond with canned responses and never hit the real APIs. That will result in the tests being reliable and fast too.

This is the point where mocking and dependency injection intertwine. If the Twitter client was instantiated in the constructor of the Tweets Retriever class with the new keyword, testing the Tweets Retriever class would be difficult. Because the Twitter Client class is injected into the constructor from outside, for the Unit test context a mock or spy can be easily injected in place of the real object. This creates a clear separation of concerns between constructing the object and using it. Dependency injection makes the code testable with writing unit tests becomes easier because any dependency can be injected at test time with a dummy implementation.

For statically typed languages like Go or Rust, as long as the type is maintained for the parameters the tests will work without any problem. In the case of dynamically typed languages like JavaScript, types are not even considered.

One difference between dependency injection and mocks is that dependency injection is related to object-oriented programming. Whereas mocks can be applied to other programming paradigms as well. For instance, if you use CommonJs modules exporting functions without using class constructs, these can be easily mocked with a testing framework like Jest or an NPM package like Rewire.

For intercepting a require or import with a Mock in the context of a unit test, you might not need to understand dependency injection concepts. But for writing easily testable object-oriented code using dependency injection is a must.

The above explanation should make the relationship between Dependency Injection and mocking for unit testing clear. In the next section, you can learn about mocking specifically for the Jest testing framework.

Mocking in Jest, an Introduction

In Jest mocking is supported out of the box - this is unlike other JavaScript testing frameworks like Mocha. It has built-in mock functions that allow you to replace the actual implementation of a function, capture calls to a function, and verify parameters passed.

Jest mock functions can also mock constructors and inject return values for mocked functions for the scope of the test. If you want to watch a method call but keep the original implementation or mock the implementation and later restore to the original implementation Jest SpyOn should be used.

In addition to mocking functions, Jest can also mock modules or just mock a subset of a module. Jest can also mock ES6 Class and its methods. Calling Classes syntactic sugar on top of prototypal inheritance and “special functions” is an opinion that can be debated as another post. For the scope of this article, you will learn how to mock Class in unit tests written with Jest. The official documentation shows 4 ways to create an ES6 class mock with Jest. You will learn 2 ways that provide a balance between both flexibility and maintainability in the next section.

Prerequisites

Before you get your hands dirty with the code, below are some good to have things:

  1. Any prior knowledge about Unit testing and Jest will be helpful
  2. Knowing the difference between CommonJs and ES modules in Node.js will be nice to have, ES6 modules will be used in this example as it is a JavaScript standard.
  3. Previous experience with mocking will be beneficial, as a pre-read going through Jest SpyOn post will be worthwhile.

All the code and tests have been run with the latest LTS version of Node.js which is 18, at the time of writing. With the requisites mentioned, you will be introduced to the example Classes in the next section.

Example Class - ExchangeRateApi Client

As an example to mock ES6 Class with Jest, you will build a simple Exchange Rate API client. This basic client calls the Open access endpoint from ExhangeRate-API. The open API is a bit limited but you can access it without any key or authentication. You can send only one currency and the data is refreshed once in 24 hours. The calls are also rate-limited at 1.5K requests per month.

This simple client ES6 class calls the ExchangeRate API for the latest exchange rate of the from currency and picks the rate for the to currency. If the exchange rate is found in the response, it returns that back else returns 0. For example, if the latest rate for USD to AUD is requested, it will get the latest rates for USD and find the AUD value in the response from the API and then send it back. If AUD is not found in the response it will return 0. The code to do this looks like the below:

export default class ExchangeRateClient {
  constructor(axios) {
    axios.defaults.baseURL = 'https://open.er-api.com/v6';
    this.axios = axios;
  }

  async getLatestExchangeRate(fromCurrency = 'USD', toCurrency = 'AUD') {
    try {
      const response = await this.axios.get(`/latest/${fromCurrency}`);
      return response.data.rates[toCurrency] || 0;
    } catch(e) {
      console.log(`Error while getting exchange rate ${e.message}`, e);
      return 0;
    }    
  }
}

The class starts with an export default to export the ES6 class as it is the only thing to be exported in this module. It has a constructor that takes the popular Axios library object. Axios is used to make the HTTP call to the API. In the constructor, the base URL is set and the Axios object is set on the class level.

The getLatestExchangeRate is the method that does the main task. It calls the API to fetch the latest exchange rates of the from currency and plucks out the rate of the to currency else returns 0. In case of any error, the catch block logs the error and returns back 0. Using the default parameters if the from and to currency pair is not passed USD and AUD will be used for them respectively. Below is a quick usage of the above Client class in the Service class. This Service class is the system under test (SUT) to understand how to mock an ES6 class with Jest:

import ExchangeRateClient from './exchangeRateClient.js';
import axios from 'axios';

export default class ExchangeRateService {
  constructor() {
    this.client = new ExchangeRateClient(axios);
  }

  async getLatestExchangeRate(fromCurrency = 'USD', toCurrency = 'AUD') {
    return this.client.getLatestExchangeRate(fromCurrency, toCurrency);
  }
}

This ExchangeRateService class is on the same level as the client file. Firstly, it imports the client and Axios. Then, in the constructor it instantiates the client passing in the needed Axios parameter.

In the getLatestExchangeRate method you call the client's getLatestExchangeRate method passing the from and the to currency. Similar to the Client, here also default parameters have been used for the from and to currency pair. The code inclusive of the tests is available on GitHub for your reference. You can clone the repository with:

git clone git@github.com:geshan/jest-mock-es6-class.git

Then go into the local folder with cd jest-mock-es6-class. To install the NPM packages you can run npm  install and to quickly test out the script you can execute:

node index.js

The above command will render and output like the below:

Exchange rate from USD to AED for today is 3.6725

The USD to AED conversion pair is chosen because AED is pegged to the US dollar so the rates don’t change. In the next section, you will grasp how to set up tests using Jest.

Setup Tests with Jest using AAA

Regardless of the language or the framework, unit tests follow a pattern of Arrange Act Assert ( AAA). First, the inputs, mocks, parameters, and targets are arranged. Then the system under test which is generally a function is called or acted on to verify the target behavior. Finally, expected outcomes are asserted after the Act. Assertions can be simple as checking value or also checking that the mock functions are called with expected parameters.

For any type of test including unit tests, assertions will eventually decide if a given unit test passes or fails.

Following the AAA pattern, you will mock ES6 class to test the behavior of the ExchangeRateService class which imports the ExchangeRateClient. As each unit test should only test the system under test, for the context of this post it will be the ExchangeRateService. To be a unit test the ExchangeRateClient class must be mocked out.

The official Jest docs list 4 ways to mock an ES6 class, they are automatic mock, manual mock, jest.mock with module factory parameters, and replacing using mock implementation. Saying the docs are convoluted would not be an overstatement. To simplify things, use the AAA pattern and follow these unit testing best practices. In the next section, you will use the module factory parameter and later mock specific methods with Jest SpyOn.

Mocking a JavaScript Class in Jest

There are multiple ways to mock an ES6 class in Jest. To keep things simple and consistent you will use the module factory parameters method and jest SpyOn to mock specific method(s) of a class. These two methods are not only flexible but also maintainable down the line.

Both methods give you the flexibility to change these mocks for each test. You will replace the whole object with the module factory pattern. It is used if you want to mock most or all of the methods in the class. If there are one or two methods to mock for the unit test, the spy method will be better suited.

You can get started with the module factory parameter mocking in the subsequent section.

Mocking with Module Factory Parameter

A module factory is a function that returns the mock. In Jest the jest.mock(path, moduleFactory) does the job by taking the module factory as the second parameter. A module factory must be a function that returns a function. It will be much easier to understand this with a code example:

import ExchangeRateClient from '../src/exchangeRateClient.js';
import ExchangeRateService from '../src/exchangeRateService.js';

const mockGetLatestExchangeRate = jest.fn().mockResolvedValueOnce(3.6725).mockResolvedValueOnce(0);
jest.mock('../src/exchangeRateClient.js', () => {
  return jest.fn().mockImplementation(() => {
    return { getLatestExchangeRate: mockGetLatestExchangeRate };
  });
});

describe('ExchangeRateService', () => {
  let service = {};

  beforeEach(() => {
    service = new ExchangeRateService();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getLatestExchangeRate', () => {
    it('should get the latest exchange rate', async ()=> {
      const latestExchangeRate = await service.getLatestExchangeRate('USD', 'AED');
      expect(latestExchangeRate).toBe(3.6725);
      expect(ExchangeRateClient).toHaveBeenCalled();

      expect(mockGetLatestExchangeRate).toHaveBeenCalled();
      expect(mockGetLatestExchangeRate).toHaveBeenCalledWith('USD', 'AED');
    });

    it('should return 0 as latest exchange rate in case of error on client', async ()=> {
      const latestExchangeRate = await service.getLatestExchangeRate('USD', 'CAD');
      expect(latestExchangeRate).toBe(0);
      expect(mockGetLatestExchangeRate).toHaveBeenCalled();
    });
  });
});

It begins by importing the ExchangeRateClient and ExchangeRateService from the src folder. The system under test for this unit test is the service class. Next, you define a constant named mockGetLatestExchangeRate which is assigned as jest.fn(). This mock will return two promise resolved values of 3.6725 and 0 respectively for the two consecutive calls.

One important thing to take note of here is the const’s name is prefixed with mock. This prefix is a requirement in Jest, since calls to jest.mock() are hoisted on the top of the file. If you name this mock variable beginning with fake or spy it will not work throwing a ReferenceError. You can learn more about JavaScript reference errors, how they happen and how to prevent them too. When writing tests be careful about JavaScript syntax errors as well.

Next, the module factory is used to mock the client class, and the factory returns a jest mock implementation function. This function returns a dummy object having the getLatestExchangeRate method which is assigned the mock function. The mock function was created in the previous line.

The main thing to remember here is, the whole client class has been mocked with a Module factory function. This factory function returns a dummy object with a method that has the mock function assigned to it.

Then the describe part is written where a service is instantiated in the beforeEach hook. This service is your system under test. After that, a very simple test is written to check the service variable is defined.

Consequently, you write a new describe named the same as the method name of getLatestExchangeRate. The first test here is to verify the getLatestExchangeRate method of the service returns an exchange rate for the given pair of from and to currency. This is the happy path.

The arrangement part has already been done ahead of all tests in the beforeEach hook and above it. The act part of calling the service’s getLatestExchangeRate methods is done here with USD and AED as two parameters for the from and to currency respectively. The test is to get the latest rate of 1 USD to AED and it is expected to be 3.6725. The value 3.6725 is the same as the first resolve value of the mock.

Next, the constructor mock for the client is expected to have been called. Similarly, the mock function is also expected to have been called and to have been called with the currency pair of USD to AED.

After that, there is another test verifying the error scenario. In case of any errors, a 0 is returned. The main difference is the value returned is asserted to be 0. The expected 0 is the second resolved value of the mock function. In the end, the mock function is again expected to have been called.

The main section of the above test is where the Exchange rate client has been replaced by the mock object using the module factory. Another noticeable part is the use of a mock function. This mock function comes in very handy later to check that it has been called. As the system under test is the service, the calls to the client class have been mocked. The constructor of the client class has also been mocked with a function that returns the dummy object.

In the next section, you will witness a different way to mock the ES6 Class for the client with Jest.spyOn to focus on the client class’ getLatestExchangeRate method.  The test’s assertions will look almost the same.

Mocking Specific Method with SpyOn

Another way to mock specific methods of an ES6 class with Jest is by using the Jest.spyOn method. It helps to selectively mock the needed method(s) without the need to mock the whole class.

Using jest spyOn to mock a specific method of an ES6 class is like performing a surgical operation. Using it you can target one or more specific methods called in the test’s context.

Below is an example code of the ES6 class mock using Jest spyOn to mock the ExchangeRateClient class' getLatestExchangeRate method only.

import ExchangeRateClient from '../src/exchangeRateClient.js';
import ExchangeRateService from '../src/exchangeRateService.js';

const getLatestExchangeRateSpy = jest.spyOn(ExchangeRateClient.prototype, 'getLatestExchangeRate')
  .mockResolvedValueOnce(3.6725)
  .mockResolvedValueOnce(0);

describe('ExchangeRateService', () => {
  let service = {};

  beforeEach(() => {
    service = new ExchangeRateService();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getLatestExchangeRate', () => {
    it('should get the latest exchange rate', async ()=> {
      const latestExchangeRate = await service.getLatestExchangeRate('USD', 'AED');
      expect(latestExchangeRate).toBe(3.6725);

      expect(getLatestExchangeRateSpy).toHaveBeenCalled();
      expect(getLatestExchangeRateSpy).toHaveBeenCalledWith('USD', 'AED');
    });

    it('should return 0 as latest exchange rate in case of error on client', async ()=> {
      const latestExchangeRate = await service.getLatestExchangeRate('USD', 'CAD');
      expect(latestExchangeRate).toBe(0);
      expect(getLatestExchangeRateSpy).toHaveBeenCalled();
    });
  });
});

This test looks similar to the above test. The main difference here is how the Client ES6 Class has been mocked. Rather than mocking the whole class, a spy has been attached to the getLatestExchangeRate method of the class’ prototype. This is done because JavaScript works with Prototypal inheritance. Regular object-oriented programming inheritance is different than Prototypal inheritance. It can be a different tangent for another post.

This makes it possible to intercept the call to the real class and doctor in the values you want the method to respond with for the test’s context. This manipulation is happening just below the imports in the above test code with jest.spyOn(ExchangeRateClient.prototype, 'getLatestExchangeRate').

The rest of the test is similar to the above test code. The other difference in this test code is there is no expectation for the constructor to be called. As the whole object with the constructor has not been mocked it is not relevant for this way of testing.

Another way to partially mock an ES6 class is with jest.requireActual. If a class has 5 methods, it can be used to mock 2 and leave the remaining 3 as actual implementation.

There you have it, two ways to mock the whole ES6 class or just a single (or more) method of a class. These methods are flexible and easily maintainable.

Conclusion

In this post, you learned about some deep concepts related to unit testing with a focus on mocking and its interwoven relation with dependency injection. Then you witnessed a simple yet useful script to get the latest currency conversion rates which involved 2 ES6 classes. Then you tested the service class that had a dependency on the client class. The client class was mocked while testing the service class.

A couple of main takeaways from this post are, always exploit dependency injection to make your unit testing easier.

There are multiple ways to mock an ES6 class in Jest, use a consistent way that works for you, and is flexible and maintainable. You can test your ES6 classes smoothly without running into unnecessary roadblocks following a consistent way.

Keep mocking those ES6 classes effortlessly as shown in the above examples.

Meticulous

Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.

Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.

Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.

Learn more here.

Authored by Geshan Manandhar

Read more