Demystify ES6 class mocking in Jest (with examples)[Intermediate]

Demystify ES6 class mocking in Jest (with examples)[Intermediate]


Lets start with why are we here first. You already have the detailed explanations for ES6 class mocks here in Jest official doc.

At times, writing tests can indeed feel intimidating, especially when you're in the midst of it. As a developer myself, I prioritize writing tests to ensure code coverage. There are moments of stress, like when your pull request isn't getting merged due to insufficient code coverage, and you're eager to push fixes quickly. During such times, you don't want to delve into intricate details but rather grasp the core concepts swiftly. This article aims to address exactly that

I hope this article dispels the fear of writing test cases for ES6 classes, as I too felt the same at one point when I was starting out.

Why do we need mocks?

Mock functions allow you to test the links between code(in our example, Company, Employee and RedisClient all are linked) by erasing the actual implementation of a function/class.

Problem: I'm facing a challenge where I need to write tests for a component, but it relies on numerous other modules that aren't my primary concern unless I'm testing the end-to-end flow. To address this, I plan to employ techniques like mocking or stubbing. These methods allow me to isolate the component under test from its dependencies, enabling focused testing without getting entangled in irrelevant details.

import Employee from "./Employee";
export class Company {
  .
  .
  employeeOnboarding = async (name: string, age: number) => {....}
  getPost = async (name: string) => {....}
 }        
import { RedisClient } from "./db/redis";
export default class Employee {
 .
 .
 addEmployee = async (name: string, age: number) => {....}
 async getEmployeePosts(name: string) {....}
}        
import Redis from "ioredis";
export class RedisClient {
 .
 .
 async getPost(key: string) {....}
 async addEmployee(key: string, value: string) {....}
}
        

Company -> Employee -> Rredis.

Clone the below repo (installation instruction are mentioned in the README file.) All the tests are under /test directory

Github repo link to the code

Mocking a class in Jest:

  1. Automatic mock : The respective code is available under automock branch in github
  2. Mocking with module factory : The respective code is available under factoryparam-mock branch in github
  3. Spying on mocks: The respective code is available under factoryparam-mock-spy branch in github

There are few others ways to mock ES classes but lets focus on the above(which will help you with your tests mostly) and once you understand this, you can easily follow the Jest docs for others way of mocking like manual mocks and replacing the mock using mockImplementation().

We can have two kinds of export module and there is a difference on how you mocks these classes. (why?? check below in 2. Mocking with module factory)

  • default class export
  • named class export

e.g you can see the Employee class is default export whereas RedisClient class is named one.

1. Automatic Mock:

To run the test for automatic mock

  • Make sure you have cloned the repo and installed the dependecies
  • Now checkout the automock branch
  • Then run the test

git checkout automock         
yarn test        

(yes you will see one test failing and will find the explanation as you scroll down)

jest.mock(<yourmodule>) returns an automatic mock and all the class methods(except arrow functions which won't be accessible in auto mocked module) are replaced by mock functions that always returns undefined. Please check below assertion in Employee.test.ts

expect(emp).toBeDefined();        

The automatic mock is special mocks as it allows you to spy on the constructor calls and on class method calls as well.

here <yourmodule> can be a custom module like Employee or RedisClient or the installed dependency module like ioredis

e.g in Company.test.ts, we are auto mocking Employee module

Company Module Test(automock):

Employee class has two methods addEmployee & getEmployeePosts

async getEmployeePosts(name: string) {...}        
addEmployee = async (name: string, age: number) => {...}        

Employee -> getEmployeePosts will return undefined when called within Company module within the test scope.

Employee -> addEmployee is an arrow function which won't be mocked and if you try to run the test on Comany module, you will get error for the first test.

yarn test /Company        

TypeError: emp.addEmployee is not a function

I won't go into details of arrow function here but if your want to know more about arrow function then do check it and here is the short explanation from Jest doc.

If you use arrow functions in your classes, they will not be part of the mock. The reason for that is that arrow functions are not present on the object's prototype, they are merely properties holding a reference to a function.

Similarly you can find complete auto mocking tests for other modules in the repo.

Employee Module Test(automock):

Redis Module Test(automock):


2. Mocking with module factory

jest.mock(path, moduleFactory)        

The module factory function passed to jest.mock is a functions that must return a function i.e it is higher order function (HOF).

The returned function from the moduleFactory can be

  • a normal function (but does not provide a way to spy on calls.)OR
  • a Jest function (This we will see in 3. Spying on mocks section)

Here we will check the normal function flow. In this flow, you can't track(which we call "to spy on") if the class constructor is called with the correct parameters. Although we can spy on class method calls and will see these in 3rd section.

Q -> Why the module factory should return a function?

A -> This will allow calling new on the function returned from module factory. Although there are some caveat which we will see later in the article.

Classes can only be instantiated with the new operator — attempting to call a class without new will throw a TypeError.

In Company.ts file, the class instance emp is created using new so we need a function in mocks on which the below call won't fail.

const emp = new Employee();        
import Employee from "./Employee";
export class Company {
  name: string;
  size: number;
  industry: string;
  constructor(name: string, size: number, industry: string) {
    this.name = name;
    this.size = size;
    this.industry = industry;
  }
  employeeOnboarding = async (name: string, age: number) => {
    const emp = new Employee();
    const result = await emp.addEmployee(name, age);
    return result;
  };
  getPost = async (name: string) => {
    .....
  };
}        

To run the test for module factory mock

  • checkout the factoryparam-mock branch
  • Then run the test

git checkout factoryparam-mock        
yarn test        

( yes all the test pass here :-) )


Employee Module (module factory):

I will explain Employee module here as it has imported RedisClient which is not a default class export and

import { RedisClient } from "./db/redis";        

we have slight change in module factory function.

jest.mock("../db/redis", () => {
  return {
    RedisClient: function () {
      return {
        getValue: () => "Mocked value",
        addValue: () => ({ status: "ok" }),
      };
    },
  };
});        

The factory function doesn't return a function rather it returns an object with the key that is same as the class export name i.e RedisClient. Hence, It will be able to import the mocked function correctly.

If you want, you can try changing the module factory to return function like below

jest.mock("../db/redis", () => {
  return function () {
    return {
      getValue: () => "Mocked value",
      addValue: () => ({ status: "ok" }),
    };
  };
});        

run the test and you will see error

TypeError: redis_1.RedisClient is not a constructor.

To see what happens if we try to spy on the RedisClient constructor or its method parameter in Employee.test.ts , lets check in 3rd section

Company Module: (module factory)


Redis Module: (module factory)

3. Spying on mocks

  • Spying on class constructor Instead of returning a function we return a jest mock function.e.g check Company.test.ts file return jest.fn().mockImplementation(.....)

Please check this explanation in Jest Doc.

In order to track calls to the constructor, replace the function returned by the HOF with a Jest mock function. Create it with jest.fn(), and then specify its implementation with mockImplementation().

To run the test for module factory spy mock

  • checkout the factoryparam-mock-spy branch
  • Then run the test

git checkout factoryparam-mock-spy        
yarn test        

( Ahh again an error , yes the error below explains the fact that if you try return a normal function from your module factory function, it doesn't allow you to spy on module constructor)

it("should call RedisClient construction - addEmployee", async () => {
    const emp = new Employee();
    const result = await emp.addEmployee("RRR", 27);
    // the below assertion will fail as the mocked factory module is an HOF which return a normal fucntion (instead of jest func)
    // that does not provide a way to spy on calls.
    expect(RedisClient).toHaveBeenCalledTimes(1);
  });        

Matcher error: received value must be a mock or spy function

In Employee.test.ts, the test fails because of this expect(RedisClient).toHaveBeenCalledTimes(1);

Employee Module(module factory spy):


Now lets see the code which allows us to spy on class methods

Company Module(module factory spy):

(mockImplementation is modified a bit here)

import Employee from "../Employee";
import { Company } from "../Company";

jest.mock("../Employee", () => {
  return jest.fn().mockImplementation(() => {
    return { 
     getEmployeePosts: () => {} ,
     addEmployee: () => {}};
    });
});        

Here is the code in test file which uses this constructor spy check this expect(Employee).toHaveBeenCalledTimes(1) in below code:

  it("should call Employee construction - getPost", async () => {
    mockGetPost.mockResolvedValueOnce("Done");
    const c = new Company("Linkedin", 20000, "Social Media");
    const posts = await c.getPost("Joe");
    expect(Employee).toHaveBeenCalledTimes(1);
  });        


  • Spying on class methods (here in Jest doc)Now our mocked factory function must provide the class methods that will be called during tests otherwise we will get an error for calling a function that doesn't exist.Company.test.ts

const mockGetPost = jest.fn();
const mockAddEmp = jest.fn();
jest.mock("../Employee", () => {
  return jest.fn().mockImplementation(() => {
    return { getEmployeePosts: mockGetPost, addEmployee: mockAddEmp };
  });
});        

A new object will be created each time the mock constructor function is called during tests. To spy on method calls in all of these objects, we populate getEmployeePosts & addEmployee with another mock function, and store a reference(e.g const mockGetPost = jest.fn()) to that same mock function in our test file, so it's available during tests.

Here is the test which spy on the class methods

  it("should get posts from given member - getPost", async () => {
    mockGetPost.mockResolvedValueOnce("Done");
    const c = new Company("Linkedin", 20000, "Social Media");
    const posts = await c.getPost("Joe");
    // the below assertion will pass as the mocked factory module is an HOF which return a jest func
    // that can be spied upon.
    expect(mockGetPost).toHaveBeenCalledTimes(1);
    expect(mockGetPost).toHaveBeenCalledWith("Joe");
    expect(posts).toBe("Done");
  });        

Redis Module (with factory mock spy)


Now that I've covered all I intended to in this article, I recommend exploring the official Jest page. I can assure you that you'll grasp each and every point about ES6 class mocks there.

Thank you for reading the article! Your feedback is valuable to me. Please feel free to leave a comment if you have any suggestions for improvement or if you spot any errors that need correction. I appreciate your input and strive to make the content as helpful and accurate as possible.

Please refer to the github repo for the complete code.

Happy Reading... Gaurav

#JestTesting #JavaScriptTesting #ES6Mocking #JestMocking #UnitTesting #MockingFramework #ES6ClassMocking #JestMockingTips #TestingInJest #SoftwareTesting #DeveloperTools #SoftwareDevelopment #WebDevTips #CareerAdvice #TechEducation #OnlineLearning

Juan Diego Sánchez

| Backend Developer | Node JS | Javascritp | Sequelize | SQL | Postman | React JS | React Native | Angular |Responsive Design | Figma |

5 个月

Excellent article! I really appreciate it

要查看或添加评论,请登录

社区洞察

其他会员也浏览了