Jest Mocking — Part 1: Function

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

Enes Başpınar
Trendyol Tech
13 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

For Turkish readers: Serinin Türkçe haline kendi blogum üzerinden ulaşabilirsiniz.

You can find the codes in the article on Github.

Source

In e-commerce applications, users can view product information, browse recommendations, purchase products, and perform many other operations. Ensuring that all of these work correctly is an important part of the software development process. That’s why we want to make sure that our code behaves as expected in every scenario. However, some scenarios may not be easy to test.

What is mock and what problem does it solve?

In fact, a mock is a imitation of anything in software.

Let’s say we want to test that the price of a discounted product is displayed correctly. At the time of writing the test, a discount is defined on the product and the test passes. However, the discount does not last forever and when it expires, things don’t go as expected. As a solution, we can create a fake product containing the data we need and use it in our test as if it were returning from the API. This way, our test is protected from fragility.

Instead, we can eliminate this chaos by replacing the function that makes requests to our own API with a fake. We would not upset either the sellers or the banks.

So we can refer as mock to each of these imitations, such as fake data, fake functions, fake APIs.

Mock Types

If you have dealt with testing before, you may have encountered the use of terms such as stub, fake, dummy instead of mock. Gerard Meszaros’s book uses the term Test Double for the fake object that replaces the real object in order to avoid confusion. Like dublors for actors in films. And he divides it into six categories:

  • Dummy: Used to fill in the parameters that we must pass to the function, but it is not used in reality.
  • Fake: Contains a simplified but working version of the code. An example of this could be reading from a object that holds test data instead of going to the database to get the hashes of passwords.
  • Stub: Used to return planned responses during function calls during testing.
  • Spy: Protects the original implementation and store extra information such as how many times the functions are called, with what parameters, and what responses are returned.
  • Mock: Used to plan what response the functions that we expect to be called with certain arguments will return. It throws an error if it is called with unexpected arguments.

However, we struggle to categorize in practice. We’ll call them all as mock.

Introduction

Our motivation when mocking functions is to manipulate the original logic, force them to return spesific values and keeping the calling history.

Let’s visualize function mocking in the code before moving with Jest.

// File: productApi.js
import axios from "axios";

async function getProduct(productId) {
const response = await axios.get(
`https://dummyjson.com/products/${productId}`
);

return response.data;
}

function mapProduct(data) {
const mappedData = { ...data };

mappedData.hotDeal = data.discountPercentage > 80;
mappedData.isRunningOut = data.stock < 5;

return mappedData;
}

export { getProduct, mapProduct };

Imagine that we have two helper functions for products and we might want to test that the hot deal exists when the product is over 80% off. We can search for a product that fit this description or we can assume it’s back.

// File: productTest.js
import axios from "axios";
import { getProduct, mapProduct } from "./productApi.js";

axios.get = async () => {
return {
data: {
id: 999,
title: "Fake iPhone 11",
description: "An apple mobile which is nothing like apple",
price: 549,
discountPercentage: 99.99,
rating: 4.69,
stock: 94,
brand: "Apple",
category: "smartphones",
thumbnail: "https://dummyjson.com/image/i/products/1/thumbnail.jpg",
images: ["https://dummyjson.com/image/i/products/1/1.jpg"],
},
};
};

async function test() {
const product = await getProduct(3);
const mappedProduct = mapProduct(product);

if (!mappedProduct.hotDeal) {
throw new Error("mapProduct method works incorrectly. take a look at the code.");
} else {
console.log("mapProduct method works correctly.");
}
}

test();

That’s the basic logic. We create the data manually and then test the function gives the expected result with this data. We don’t want to use the real value because data can be difficult to find, and our test can become fragile with API-related problems.

Mocking Function

Now we can continue with Jest. We use jest.fn(implementation?) to create a mock function.

// File: mockFunction.test.js
test("playground", () => {
const mockFunction = jest.fn();
console.log("mockFunction:", mockFunction);
});

/* OUTPUT:
mockFunction: [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)]
}
*/

We can override the methods with the mock functions.

// File: overridePackageMethod.test.js
import axios from "axios";

test("playground", () => {
axios.get = jest.fn();
console.log("mock Implementation:", axios.get.toString());
});

/* OUTPUT:
mock Implementation: function () {
return fn.apply(this, arguments);
}
*/

Also we can mock built-in object’s methods.

// File: builtInMethod.test.js
test("playground", () => {
console.log("original Implementation:", Math.random.toString());
Math.random = jest.fn();
console.log("mock Implementation:", Math.random.toString());
});

/* OUTPUT:
original Implementation: function random() { [native code] }
mock Implementation: function () {
return fn.apply(this, arguments);
}
*/

.mock Property

Mock functions store extra information that we can use in tests. We can access these with the .mock property.

  • mock.calls: List arguments the function was called with.
  • mock.results: List results of the function calls. The return type can be return, throw or incomplete.
  • mock.instances: List instances if the function is a constructor.
  • mock.contexts: List this objects at the time the function was called.
  • mock.lastCall: List arguments of the last call.
// File: manipulateArray.test.js
function manipulateArray(array, manipulateMethod) {
return array.map((item) => manipulateMethod(item));
}

test("playground", () => {
const array = [0, 1, 2];
const mockManipulateMethod = jest.fn((x) => x + 2);
manipulateArray(array, mockManipulateMethod);

console.log(
"mockManipulateMethod's mock property:",
mockManipulateMethod.mock
);
});

/* OUTPUT:
mockManipulateMethod's mock property: {
"calls": [[0], [1], [2]],
"contexts": [null, null, null],
"instances": [null, null, null],
"results": [
{ "type": "return", "value": 2 },
{ "type": "return", "value": 3 },
{ "type": "return", "value": 4 },
],
"lastCall": [2]
}
*/

In the rest of the article, we will see .mock on real examples.

Returning Static Value

We have two different methods for forcing the mock function to return a static value:

  • mockReturnValue(value) - Define the value to be returned on all calls.
  • mockReturnValueOnce(value) - Define the value to be returned on next call.
// File: mockReturns.test.js
test("playground", () => {
const mockFunction = jest
.fn()
.mockReturnValue("other calls")
.mockReturnValueOnce("first call")
.mockReturnValueOnce("second call");

for (let index = 0; index < 5; index++) {
console.log("mockedProduct", mockFunction());
}
});

/* OUTPUT:
mockedProduct first call
mockedProduct second call
mockedProduct other calls
mockedProduct other calls
mockedProduct other calls
*/

Let’s look at two more examples.

In the first example, let’s say we have a function to read a value from local storage. We will mock the window.localStorage.getItem method to test whether our method returns the correct value when we give it a key.

// File: getFromLocalStorage.test.js
function getFromLocalStorage(key) {
return window.localStorage.getItem(key);
}

test("should get data from local storage correctly", () => {
const key = "testKey";
const value = "testValue";

const mockLocalStorageGet = jest.fn();

Object.defineProperty(window, "localStorage", {
value: {
getItem: mockLocalStorageGet,
},
});

// we want mock value to be returned when mock localstorage is called.
mockLocalStorageGet.mockReturnValue(value);
// another usage: window.localStorage.getItem.mockReturnValue(value)

getFromLocalStorage(key);

expect(jest.isMockFunction(window.localStorage.getItem)).toBe(true)
expect(mockLocalStorageGet.mock.lastCall[0]).toBe(key);
expect(mockLocalStorageGet.mock.results[0].value).toBe(value);
});

/* OUTPUT:
PASS getFromLocalStorage.test.js
✓ should get data from local storage correctly (3 ms)
*/

In the second example, we want to calculate the remaining time to a future date. If we don’t mock it, the test will pass until the end date. We must not allow fragile tests. That’s why we’ll mock the value returned by new Date() and make sure it's before the end date.

// File: getRemainingTime.test.js
function getRemainingTime(endDate, startDate = new Date()) {
let delta = (endDate.getTime() - startDate.getTime()) / 1000;

return {
remainingDays: Math.floor(delta / (60 * 60 * 24)),
remainingHours: Math.floor((delta / (60 * 60)) % 24),
remainingMinutes: Math.floor((delta / 60) % 60),
remainingSeconds: Math.floor(delta % 60),
};
}

test("should return remaining data when give future date", () => {
const endDate = new Date(2023, 1, 1);
const mockCurrDate = new Date(2022, 10, 16, 16, 9, 25);

// we want the mock date to be returned when the constructor is called.
global.Date = jest.fn().mockReturnValue(mockCurrDate);

expect(getRemainingTime(endDate)).toEqual({
remainingDays: 76,
remainingHours: 7,
remainingMinutes: 50,
remainingSeconds: 35,
});
});

/* OUTPUT:
PASS getRemainingTime.test.js
✓ should return remaining data when give future date (3 ms)
*/

As a unit test concern, we should mock the all external functions (regardless of whether they are built-in or imported) used in the function we want to test. This is because the logic of those methods is already being tested in their own files, and you want to focus on testing the logic of the function you are testing. Mocking external functions allows you to isolate the function you are testing and ensure that it is functioning correctly without interference from other external dependencies.

Returning Dynamic Value

We may want to change the return value of the mock function according to its arguments. We can define implementation. We have three methods for this:

  • mockImplementation(func): This method sets the implementation of the mock function that will be used for all calls.
  • mockImplementationOnce(func): This method sets the implementation of the mock function that will be used for a single call. After the mock function is called once, it will revert to its default behavior (which is to return undefined).
  • withImplementation(func, callback): This method sets the implementation of the mock function that will be used for all invocations that occur within the scope of the provided callback function.

Let’s look at the examples.

In the first example, we want to change the implementation of a function that applies the given function to the elements of the array.

// File: manipulateArray.test.js
function manipulateArray(array, manipulateMethod) {
return array.map((item) => manipulateMethod(item));
}

test("playground", () => {
const array = [0, 1, 2];
const manipulateMethod = jest.fn().mockImplementation((x) => x + 2);
manipulateArray(array, manipulateMethod);

console.log(manipulateMethod.mock.results);
});

/* OUTPUT:
[
{ type: 'return', value: 2 },
{ type: 'return', value: 3 },
{ type: 'return', value: 4 }
]
*/

In the second example, we can see how we can change the implementation within the specific block.

// File: withImplementation.test.js
test("playground", () => {
const mockMethod = jest.fn(() => "outside callback");

console.log(mockMethod());

mockMethod.withImplementation(
// temporary implementation for the mock function
() => "inside callback",
// the scope in which the implementation will be applied
() => {
console.log(mockMethod());
}
);

console.log(mockMethod());
});

/* OUTPUT:
outside callback
inside callback
outside callback
*/

Mocking Asynchronous Functions

Asynchronous functions, as we know, return Promise. So, the mock function should also return a Promise. We can do this with jest.fn().mockImplementation(() => Promise.resolve(value)) or jest.fn().mockImplementation(() => Promise.reject(value)). However, Jest provides four methods that abstract these implementations:

  • mockResolvedValue(value): Always returns a resolved Promise with the given value.
  • mockResolvedValueOnce(value): Returns a resolved Promise with the given value for the first call, and then the default implementation for next calls.
  • mockRejectedValue(value): Always returns a rejected Promise with the given value.
  • mockRejectedValueOnce(value): Returns a rejected Promise with the given value for the first call, and then the default implementation for next calls.

Let’s look at an example.

// File: getProduct.test.js
import axios from "axios";

async function getProduct(productId) {
try {
const response = await axios.get(`https://dummyjson.com/products/${productId}`);

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

describe("getProduct tests", () => {
beforeEach(() => {
axios.get = jest.fn();
});

test("should be return product data when request is succesfully", async () => {
const mockedValue = {
data: {
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",
thumbnail: "https://dummyjson.com/image/i/products/1/thumbnail.jpg",
images: [
"https://dummyjson.com/image/i/products/1/1.jpg",
"https://dummyjson.com/image/i/products/1/2.jpg",
"https://dummyjson.com/image/i/products/1/3.jpg",
"https://dummyjson.com/image/i/products/1/4.jpg",
"https://dummyjson.com/image/i/products/1/thumbnail.jpg",
],
},
};
axios.get.mockResolvedValue(mockedValue);

const result = await getProduct();

expect(result).toStrictEqual(mockedValue.data);
});

test("should be return product data when request is failed", async () => {
axios.get.mockRejectedValue(new Error("Error occured when fetching data!"));

const result = await getProduct();

expect(result).toStrictEqual(null);
});
});

/* OUTPUT:
PASS sgetProduct.test.js
getProduct tests
✓ should be return product data when request is succesfully (20 ms)
✓ should be return product data when request is failed (1 ms)
*/

Cleaning Mock History

Sometimes we write multiple tests to check the results of methods in different cases (as in the previous code). Jest does not clean the mock data by default. This also causes errors. For example:

// File: afterEachless.test.js
describe("playground", () => {
test("test 1", () => {
Math.random = jest.fn().mockReturnValue(55);

console.log("first random value: ", Math.random());
console.log("second random value: ", Math.random());

console.log(Math.random.mock.calls.length);
});

test("test 2", () => {
console.log("third random value: ", Math.random());
console.log("fourth random value: ", Math.random());

console.log(Math.random.mock.calls.length);
});
});

/* OUTPUT:
first random value: 55
second random value: 55
2
third random value: 55
fourth random value: 55
4
*/

As you can see, the mocked function in a test is also affecting the others. To get rid of this situation, we have three different methods:

  • mockClear: Clears the data in the .mock property.
  • mockReset: In addition to mockClear, clears the effect of mockReturnValue and mockImplementation type functions.
  • mockRestore: In addition to mockReset, it restores the original implementation when mock was created with jest.spyOn.

If you want to clear the history of all our mock functions instead of a single mock function, you can use jest.clearAllMocks, jest.resetAllMocks and jest.restoreAllMocks methods. Even if you don’t want to write it in every test file, you can activate the clearMocks, resetMocks, restoreMocks rules in the jest.config.js file.

We usually prefer to use it with afterEach as we want to clean after tests. For example:

// File: afterEach.test.js
describe("playground", () => {
afterEach(() => {
Math.random.mockRestore();
});

test("test 1", () => {
Math.random = jest.fn().mockReturnValue(55);

console.log("first random value:", Math.random());
console.log("second random value:", Math.random());

console.log(Math.random.mock.calls.length);
});

test("test 2", () => {
console.log("third random value:", Math.random());
console.log("fourth random value:", Math.random());

console.log(Math.random.mock.calls.length);
});
});

/* OUTPUT:
first random value: 55
second random value: 55
2
third random value: undefined
fourth random value: undefined
2
*/

Oh, nooooo. The mock data has been cleared between tests. However, it was unable to restore to original implementation. Why?

Spy

If we override the methods of a mock function, we lose access to the original implementation. Instead, we can use jest.spyOn(object, methodName).

This method uses the original implementation of the function by default, but like jest.fn, it allows us to access details of the calls. The returned value is also a mock function and has all the methods available for jest.fn.

// File: dateNow.test.js
test("playground", () => {
const dateNowSpy = jest.spyOn(Date, "now");

console.log("mocked function:", Date.now);
console.log("first call return value:", Date.now());

dateNowSpy.mockReturnValueOnce(500);

console.log("second call return value:", Date.now());
console.log("third call return value:", Date.now());
});

/* OUTPUT:
mocked function: [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)]
}

first call return value: 1671971862021
second call return value: 500
third call return value: 1671971862027
*/

As we can see, it does not toucdateh the original implementation. If we want to override it, we know the methods we need to use.

// File: dateNowImplementation.test.js
test("playground", () => {
jest.spyOn(global.Date, "now");
console.log("first call return value: ", Date.now());

global.Date.now.mockImplementation(() => "Hacked by crazyboy!");
console.log("second call return value: ", Date.now());
console.log("third call return value: ", Date.now());

global.Date.now.mockRestore();
console.log("fourth call return value: ", Date.now());
});

/* OUTPUT:
first call return value: 1671972138129
second call return value: Hacked by crazyboy!
third call return value: Hacked by crazyboy!
fourth call return value: 1671972138151
*/

If we want to track when and with what parameters a function has been called and what result it has returned while continuing to perform its original function, it’s the perfect fit.

TypeScript Type Support

If you are using TypeScript, the following code will give you an error saying that there is no mock property on the type of getItem.

test("playground", () => {
const mockLocalStorageGet = jest.fn();

Object.defineProperty(window, "localStorage", {
value: {
getItem: mockLocalStorageGet,
},
});

console.log(window.localStorage.getItem.mock.results); // TS Error!
});

To solve this problem, you can use the jest.Mock type or the jest.mocked method (wraps with mock types).

// usage 1 (my favorite):
const mockedLocalStorageGetItem = jest.mocked(window.localStorage.getItem)

// usage 2:
const mockedLocalStorageGetItem = window.localStorage.getItem as jest.Mock;

Special Expect Methods

We don’t usually manually check values in the .mock property. Jest provides helpful methods to make our work easier.

Related with the number of calls:

  • toHaveBeenCalled(): Checks if the mock function was called
  • toHaveBeenCalledTimes(number): Checks if the mock function was called the specified number of times

Related with the arguments of calls:

  • toHaveBeenCalledWith(arg1, arg2, …) : Checks if the mock function was called with the specified arguments
  • toHaveBeenLastCalledWith(arg1, arg2, …) : Checks if the mock function was last called with the specified arguments
  • toHaveBeenNthCalledWith(nthCall, arg1, arg2, ...): Checks if the mock function was called with the specified arguments on the nth call

Related with the return values of calls:

  • toHaveReturned() : Checks if the mock function returned a value (not throwing an error)
  • toHaveReturnedTimes(number) : Checks if the mock function returned the specified number of times
  • toHaveReturnedWith(value) : Checks if the mock function returned the specified value
  • toHaveLastReturnedWith(value): Checks if the mock function last returned the specified value
  • toHaveNthReturnedWith(nthCall, value): Checks if the mock function returned the specified value on the nth call
// File: expect.test.js
test("playground", async () => {
const mockAreaCalculate = jest.fn((x, y) => x * y);

mockAreaCalculate(7, 13);
mockAreaCalculate(3, 5);
mockAreaCalculate(5, 8);

expect(mockAreaCalculate).toHaveBeenCalled();
expect(mockAreaCalculate).toHaveBeenCalledTimes(3);
expect(mockAreaCalculate).toHaveBeenCalledWith(7, 13);
expect(mockAreaCalculate).toHaveBeenLastCalledWith(5, 8);
expect(mockAreaCalculate).toHaveBeenNthCalledWith(2, 3, 5);

expect(mockAreaCalculate).toHaveReturned();
expect(mockAreaCalculate).toHaveReturnedTimes(3);
expect(mockAreaCalculate).toHaveReturnedWith(7 * 13);
expect(mockAreaCalculate).toHaveLastReturnedWith(5 * 8);
expect(mockAreaCalculate).toHaveNthReturnedWith(2, 3 * 5);
});

/* OUTPUT:
PASS expect.test.js
✓ playground
*/

To test partially the arguments or returned values of a mock function, we can use expect properties.

  • expect.anything(): Matches any value except for null and undefined.
  • expect.any(constructor): Matches any value created by the given constructor.
  • expect.stringContaining(string): Matches if the given string is contained.
  • expect.stringMatching(string | regexp): Matches if the given string or regex matches.
  • expect.arrayContaining(array): Matches if the array contains the given subset.
  • expect.objectContaining(object): Matches if the object contains the given subset.
  • expect.not.stringContaining(string): Matches if the value is not a string or does not contain the given string.
  • expect.not.stringMatching(string | regexp): Matches if the value is not a string or does not match the given string or regex.
  • expect.not.arrayContaining(array): Matches if the array does not contain the given subset.
  • expect.not.objectContaining(object): Matches if the object does not contain the given subset.
// File: matchers.test.js
test("playground", async () => {
const calledArguments = [
[1, 2, 3],
{
name: "Sherlock",
surname: "Holmes",
job: "Consulting Detective",
partner: "Dr. Watson",
},
];

expect(calledArguments).toEqual([
expect.arrayContaining([1, 3]),
expect.objectContaining({
name: expect.anything(),
job: expect.stringContaining("Detective"),
partner: expect.any(String),
}),
]);
});

/* OUTPUT:
PASS matchers.test.js
✓ playground
*/

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

Let’s end the article with my meme.

Resources

--

--