Demystify ES6 class mocking in Jest (with examples)[Intermediate]
Gaurav Kumar Singh
Software Engineer | JavaScript Pro | React Pro | Node Pro | Databases Expert | xQL expert | Python | Java | K8s | AWS | Bigdata | ES | AI-ML
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
Mocking a class in Jest:
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)
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
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
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
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
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
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);
});
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
| Backend Developer | Node JS | Javascritp | Sequelize | SQL | Postman | React JS | React Native | Angular |Responsive Design | Figma |
5 个月Excellent article! I really appreciate it