Jest Mocking — Part 2: Module

In this article series, we will take a look at how to mock with Jest.

Enes Başpınar
Trendyol Tech
11 min readJan 17, 2023

--

Jest Mocking — Part 1: Function
Jest Mocking — Part 2: Module
Jest Mocking — Part 3: Timer
Jest Mocking — Part 4: React Component

Thanks to my friend Ali Balbars for his help in writing the English version of this article.

You can find the codes in the article on Github.

Source

We talked about how to mock the methods of objects. Now let’s look at how we can mock the function modules.

Introduction

What is Module?

We keep the code that performs an action in functions. This allows us to apply abstraction. We can also group related functions, variables, objects etc. in a file. These files are called modules.

There are two different module mechanisms that we use most often in Node.js. It will be very useful for us to be familiar with what they return when imported using different methods when we are mocking.

CommonJs:

// File: utils.ts
const axios = require("axios");

const API_URL = "https://dummyjson.com";

async function get(apiUrl: string): Promise<any> {
try {
const response = await axios.get(apiUrl);

return response.data;
} catch (error) {
return null;
}
}

function getProduct(productId: number): Promise<any> {
return get(`${API_URL}/products/${productId}`);
}

function getUser(userId: number): Promise<any> {
return get(`${API_URL}/users/${userId}`);
}

module.exports = {
get,
getUser,
getProduct,
};
// File: utils.test.ts
const utils = require("./utils");

test("playground", () => {
console.log("require as normal:", utils);
}

/* OUTPUT:
require as normal: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct],
default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
}
*/

ES Modules:

// File: utils.ts
import axios from "axios";

const API_URL = "https://dummyjson.com";

async function get(apiUrl: string): Promise<any> {
try {
const response = await axios.get(apiUrl);

return response.data;
} catch (error) {
return null;
}
}

function getProduct(productId: number): Promise<any> {
return get(`${API_URL}/products/${productId}`);
}

function getUser(userId: number): Promise<any> {
return get(`${API_URL}/users/${userId}`);
}

export { get, getUser, getProduct };

export default {
get,
getUser,
getProduct,
};
// File: utils.test.ts
import * as utilsWithStar from "./utils";
import utilsWithDefault, from "./utils";

test("playground", () => {
console.log("import with * as:", utilsWithStar);
console.log("import as default:", utilsWithDefault);
});

/* OUTPUT:
import with * as: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct],
default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
}

import as default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
*/

We will use the import * assyntax a lot in tests.

Module Mocking

Let’s say we have helper functions to pull product and user information.

Dividing code blocks into action-based functions (abstracting), like these methods is an essential part of the testing.

// File: utils.ts
import axios from "axios";

const API_URL = "https://dummyjson.com";

export async function get(apiUrl: string): Promise<any> {
try {
const response = await axios.get(apiUrl);

return response.data;
} catch (error) {
return null;
}
}

export function getProduct(productId: number): Promise<any> {
return get(`${API_URL}/products/${productId}`);
}

export function getUser(userId: number): Promise<any> {
return get(`${API_URL}/users/${userId}`);
}

Let’s try mocking with the information we have. The first thing that comes to mind may be to override by importing.

// File: overrideWithMock.test.ts
import { get } from "./utils";

test("should be mock", () => {
get = jest.fn();

expect(jest.isMockFunction(get)).toBe(true);
});

/* OUTPUT:
error TS2632: Cannot assign to 'get' because it is an import.
*/

We encountered a error that says the function we are trying to mock is read-only import. Importing the file as an object might solve the problem.

// File: overrideObjectWithMock.test.ts
import * as UtilsModule from "./utils";

test("should be mock", () => {
UtilsModule.get = jest.fn();

expect(jest.isMockFunction(UtilsModule.get)).toBe(true);
});

/* OUTPUT:
error TS2540: Cannot assign to 'get' because it is a read-only property.
*/

However, we still get a TypeScript error. Nice try. Even if it were possible, it wouldn’t be a good practice to directly override the Import. Let’s leave this to the skilled hands of Jest.

jest.mock()

We can use jest.mock(relativeFilePath, factory, options) for module mocking. If only the file path is given, it automatically mocks all exported methods with jest.fn.

// File: utils.test.ts
import * as UtilsModule from "./utils";

jest.mock("./utils");

test("playground", () => {
console.log("utils Module:", UtilsModule);
expect(jest.isMockFunction(UtilsModule.get)).toBe(true);
});

/* OUTPUT:
utils Module: {
__esModule: true,
getProduct: [Function: getProduct] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: bound withImplementation],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
},
getUser: [Function: getUser] {
_isMockFunction: true,
...
},
default: [Function: get] {
_isMockFunction: true,
...
}
}

PASS utils.test.ts
✓ playground (53 ms)
*/

We mocked after calling import. How did this happen?

Always it is necessary to mock any object before using it. In order to follow the rule of having import statements at the top of the file, Jest hoists jest.mock statements to preserve this structure.

Mocking the Implementation of Module Methods

Since we know that methods are automatically mocked with jest.fn, we can use methods mentioned in the previous article.

Let’s start the tests of get method in Utils module. If we examine it, we can see that it uses the axios package. It is good practice to mock the other functions that the function being tested depends on.

// File: utils.test.ts
import axios from "axios";
import * as UtilsModule from "./utils";

// mock axios package.
jest.mock("axios");

// wrap jest mock method types to package.
const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
afterEach(() => {
jest.clearAllMocks();
});

describe("get() tests", () => {
test("should return product when request is success", async () => {
const apiUrl = "https://dummyjson.com/product/1";
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

// make the axios.get function return mock data.
mockedAxios.get.mockResolvedValueOnce({
data: mockProduct,
});

// call the function we are going to test.
const result = await UtilsModule.get(apiUrl);

expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
expect(result).toStrictEqual(mockProduct);
});

test("should return null when request is failed", async () => {
const apiUrl = "https://dummyjson.com/product/1000";

mockedAxios.get.mockRejectedValueOnce(
new Error("Error occured when fetching data!")
);

const result = await UtilsModule.get(apiUrl);

expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
expect(result).toBeNull();
});
});
});

Jest does not add TypeScript types for own methods after mocking the modules. If we want to wrap the types of Jest methods in all the functions it contains, we can use the jest.mocked(source) method.

Factory Parameter

If we want to manually mock, we can use the factory parameter. Let’s modify the example to reject the request if the return value is not mocked, and refactor the second test.

// File: utils.test.ts
import axios from "axios";
import * as UtilsModule from "./utils";

jest.mock("axios", () => {
return {
get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
};
});

const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
afterEach(() => {
jest.clearAllMocks();
});

describe("get() tests", () => {
test("should return product when request is success", async () => {
const apiUrl = "https://dummyjson.com/product/1";
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

console.log("mockedAxios", mockedAxios);

mockedAxios.get.mockResolvedValueOnce({
data: mockProduct,
});

const result = await UtilsModule.get(apiUrl);

expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
expect(result).toStrictEqual(mockProduct);
});

test("should return null when request is failed", async () => {
const apiUrl = "https://dummyjson.com/product/1000";

const result = await UtilsModule.get(apiUrl);

expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
expect(result).toBeNull();
});
});
});

/*
PASS src/tests/utils.test.ts
utils tests
get() tests
✓ should return product whe request is success
✓ should return null when request is failed
*/

If we need to define a default mock value or perform partial mocking, we can use the factory.

An important point to consider is how mocks work together with factories and tests. Let’s look at three different uses.

// File: mock-order-1.test.ts
import axios from "axios";

jest.mock("axios", () => {
return {
get: jest.fn().mockResolvedValue("Mock in module factory"),
};
});

const mockedAxios = jest.mocked(axios);

test("playground", async () => {
const apiUrl = "https://dummyjson.com";

mockedAxios.get.mockResolvedValue("Mock in test");
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
});
// File: mock-order-2.test.ts
import axios from "axios";

jest.mock("axios", () => {
return {
get: jest.fn().mockResolvedValue("Mock in module factory"),
};
});

const mockedAxios = jest.mocked(axios);

test("playground", async () => {
const apiUrl = "https://dummyjson.com";

mockedAxios.get.mockResolvedValueOnce("Mock in test");
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
});
// File: mock-order-3.test.ts
import axios from "axios";

jest.mock("axios", () => {
return {
get: jest.fn().mockResolvedValueOnce("Mock in module factory"),
};
});

const mockedAxios = jest.mocked(axios);

test("playground", async () => {
const apiUrl = "https://dummyjson.com";

mockedAxios.get.mockResolvedValue("Mock in test");
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
});
// File: mock-order-4.test.ts
import axios from "axios";

jest.mock("axios", () => {
return {
get: jest.fn().mockResolvedValueOnce("Mock in module factory"),
};
});

const mockedAxios = jest.mocked(axios);

test("playground", async () => {
const apiUrl = "https://dummyjson.com";

mockedAxios.get.mockResolvedValueOnce("Mock in test");
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
});

Partial Mocking

So far, we have mocked the entire module. However, we may want to mock only spesific functions. To access the real content of the module, jest.requireActual is used.

jest.mock('axios', () => {
const originalAxiosModule = jest.requireActual('axios');

return {
__esModule: true,
...originalAxiosModule,
get: jest.fn(),
};
});

Mocking Module for Specific Tests

Let’s move on to the getProduct method test. We will mock the get function used in getProduct.

// File: utils.test.ts
import axios from "axios";
import * as UtilsModule from "./utils";

jest.mock("./utils");
jest.mock("axios", () => {
return {
get: jest
.fn()
.mockRejectedValue(new Error("Error occured when fetching data!")),
};
});

const mockedUtils = jest.mocked(UtilsModule);
const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
// ...

describe("getProduct() tests", () => {
test("should call get func with api product endpoint when given product id", () => {
const productId = 1;
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

mockedUtils.get.mockResolvedValue(mockProduct);

const result = UtilsModule.getProduct(productId);

expect(UtilsModule.get).toHaveBeenCalledWith(
`https://dummyjson.com/products/${productId}`
);
expect(result).toStrictEqual(mockProduct);
});
});
});

/* OUTPUT:
utils tests
get() tests
✕ should return product whe request is success (4 ms)
✕ should return null when request is failed
getProduct() tests
✕ should call get func with api product endpoint when given product id

● utils tests › get() tests › should return product whe request is success
Expected: "https://dummyjson.com/product/1"
Number of calls: 0

● utils tests › get() tests › should return null when request is failed
Expected: "https://dummyjson.com/product/1000"
Number of calls: 0

● utils tests › getProduct() tests › should call get func with api product endpoint when given product id
Expected: "https://dummyjson.com/products/1"
Number of calls: 0
*/

Our test has failed and also previous tests have also failed. Because the mocked function are not being called. We have mocked the Utils module for the getProduct method. The get and getProduct methods should not be mocked in their own tests.

It would be useful to be able to mock functions on a test-by-test basis. We can solve this problem using several different methods.

jest.doMock()

Another way to mock a module in Jest is to use jest.doMock. The difference from jest.mock is that it is not hoisted. That is, it only mocks imports written after itself.

// File: utils.test.ts
import axios from "axios";
import UtilsModule from "./utils";

jest.mock("axios", () => {
return {
get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
};
});

const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
afterEach(() => {
jest.clearAllMocks();
jest.resetModules(); // clears all module mocks in this file.
});

//...

describe("getProduct() tests", () => {
test("should call get func with api product endpoint when given product id", () => {
const productId = 1;
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

jest.doMock("./utils", () => ({
__esModule: true,
...jest.requireActual("./utils"),
get: jest.fn().mockResolvedValue(mockProduct),
}));

// this is a critical point. we import module with require we
// made afterwards befacuse of doMock will not be hoisted
const GetModule = require("./utils");
const UtilsModule = require("./utils");

const result = UtilsModule.getProduct(productId);

expect(GetModule.default).toHaveBeenCalledWith(`https://dummyjson.com/products/${productId}`);
expect(result).toStrictEqual(mockProduct);
});
});
});

We received an error that we were unable to mock the get method. This happens because getProduct and get are located in the same file and we are expecting an import to mock them. To confirm this, let's write out the mockedUtils variable.

// File: utils.test.ts
test("should call get func with api product endpoint when given product id", () => {
// ...
const UtilsModule = require("./utils");

console.log("get:", UtilsModule.get);
// ...
})

/* OUTPUT:
get: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: bound withImplementation],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
*/

Now let’s write out the get method inside getProduct method.

export function getProduct(productId: number): Promise<any> {
console.log("get Function: ", get.toString());
// ...
}

/* OUTPUT:
get Function: [Function: get]
*/

As we thought, the method was not mocked. We can extract it to a separate file and mock it through the new file.

// File: get.ts
import axios from "axios";

export default async function get(apiUrl: string): Promise<any> {
try {
const response = await axios.get(apiUrl);

return response.data;
} catch (error) {
return null;
}
}
// File: utils.test.ts
import axios from "axios";
import * as GetModule from "./get";

jest.mock("axios", () => {
return {
get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
};
});

const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
afterEach(() => {
jest.clearAllMocks();
jest.resetModules(); // clears all module mocks in this file.
});

//...

describe("getProduct() tests", () => {
test("should call get func with api product endpoint when given product id", async () => {
const productId = 1;
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

jest.doMock("./get", () => {
return {
__esModule: true,
default: jest.fn().mockResolvedValue(mockProduct),
};
});
const GetModule = require("./get");
const UtilsModule = require("./utils");

const result = await UtilsModule.getProduct(productId);

expect(GetModule.default).toHaveBeenCalledWith(`https://dummyjson.com/products/${productId}`);
expect(result).toStrictEqual(mockProduct);
});
});
});

/* OUTPUT:
utils tests
get() tests
✓ should return product whe request is success (4 ms)
✓ should return null when request is failed
getProduct() tests
✓ should call get func with api product endpoint when given product id
*/

jest.spyOn()

Remember that we mocked object methods using jest.spyOn. We can also mock modules by import with import * as as object. While extracting to a separate file is still necessary, it is a cleaner approach.

// File: utils.test.ts
import axios from "axios";
import * as GetModule from "./get";
import * as UtilsModule from "./utils";

jest.mock("axios", () => {
return {
get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
};
});

const mockedAxios = jest.mocked(axios);

describe("utils tests", () => {
afterEach(() => {
jest.clearAllMocks();
jest.resetModules(); // clears all module mocks in this file.
});

//...

describe("getProduct() tests", () => {
test("should call get func with api product endpoint when given product id", async () => {
const productId = 1;
const mockProduct = {
id: 1,
title: "iPhone 9",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
};

jest.spyOn(GetModule, "default").mockResolvedValue(mockProduct);

const result = await UtilsModule.getProduct(productId);

expect(GetModule.default).toHaveBeenCalledWith(`https://dummyjson.com/products/${productId}`);
expect(result).toStrictEqual(mockProduct);
});
});
});

/* OUTPUT:
utils tests
get() tests
✓ should return product whe request is success (4 ms)
✓ should return null when request is failed
getProduct() tests
✓ should call get func with api product endpoint when given product id
*/

Cleaning Modul Mock

We have mocked the module but may want to use its actual content for some tests. We have two methods for this. jest.dontMock clears the mocks of import objects that come after it.

// File: dontMock.test.ts
jest.mock("axios");

test("playground", () => {
const axiosInstance1 = require("axios"); // mocked
console.log(
"Is axiosInstance1.get mocked:",
jest.isMockFunction(axiosInstance1.get)
);

jest.dontMock("axios");

const axiosInstance2 = require("axios"); // unmocked
console.log(
"Is axiosInstance2.get mocked:",
jest.isMockFunction(axiosInstance2.get)
);
});

/* OUTPUT:
Is axiosInstance1.get mocked: true
Is axiosInstance2.get mocked: false
*/

jest.unmock clears the mocks of all relevant import objects in the code block it is in.

// File: unmock.test.ts
jest.mock("axios");

test("playground", () => {
const axiosInstance1 = require("axios"); // mocked
console.log(
"Is axiosInstance1.get mocked:",
jest.isMockFunction(axiosInstance1.get)
);

jest.unmock("axios");

const axiosInstance2 = require("axios"); // unmocked
console.log(
"Is axiosInstance2.get mocked:",
jest.isMockFunction(axiosInstance2.get)
);
});

/* Output:
Is axiosInstance1.get mocked: false
Is axiosInstance2.get mocked: false
*/

We talked about how to mock modules. In the next article, we will talk about timers.

Resources

--

--