Testing with Jest
Mocking classes/modules
Classes
Let’ say we have this class:
// database.js
class Database {
connect() {}
save(data) {}
}
Then to mock:
import Database from "./database";
// This will mock the whole Database class, replacing all methods with jest mock functions.
jest.mock("./database");
test("should use mocked save method", () => {
const dbInstance = new Database();
// Mocking the save method with a specific return value
dbInstance.save.mockReturnValue(true);
const result = dbInstance.save({ key: "value" });
expect(result).toBe(true);
expect(dbInstance.save).toHaveBeenCalledWith({ key: "value" });
// The connect method is still a mock function (but without a specific behavior).
dbInstance.connect();
expect(dbInstance.connect).toHaveBeenCalled();
});
Modules
Say we have the following module file:
// utils.js
export const doSomething = () => {
// ...
};
export const fetchUserData = async (userId) => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
};
Mocked:
jest.mock("./utils", () => {
return {
doSomething: jest.fn(() => "mocked doSomething"),
fetchUserData: jest.fn((userId) =>
Promise.resolve({ id: userId, name: "Mock User" })
),
};
});
test("should use mocked module functions", () => {
expect(utils.doSomething()).toBe("mocked doSomething");
expect(utils.doSomething).toHaveBeenCalled();
const result = await utils.fetchUserData(123);
expect(result).toEqual({ id: 123, name: "Mock User" });
expect(utils.fetchUserData).toHaveBeenCalledWith(123);
});
Inline mocking versus “per test” mocking
There are two different architectures that we can use when mocking modules and classes: inline and per test mocking.
Here is the inline case:
jest.mock("./some_module.js", () => {
return {
someFunction: jest.fn(() => "value"),
someFunctionWithParam: jest.fn((param) => ({
property: param,
})),
someAsyncFunction: jest.fn(() => Promise.resolve("value")),
};
});
Here is the per test case:
import { someModule } from "./some_module.js";
let someModuleMock;
someModuleMock = {
someFunction: jest.fn(() => "value"),
someFunctionWithParam: jest.fn((param) => ({
property: param,
})),
someAsyncFunction: jest.fn(() => Promise.resolve("value")),
};
someModule.mockImplementation(() => someModuleMock);
it("should do something", () => {
const newValue = "new value";
someModule.someFunction.mockReturnValue(newValue);
});
The benefits of inline:
- Inline is good because everything is set up in one place
- Inline keeps consistency accross tests: every test case in the file will use the same mocked function unless overwritten within a test
- It lends itself to being a global mock that can be used accross test files in a
__mocks__/
directory
The benefits of per-test:
- You can very mock implementations within the file, providing more granular control. You can redefine
someModuleMock
or parts of it (someModule.someFunction
) throughout your test file to accomodate varied requirements between tests - It’s beneficial when your tests have divergent requirements, as you can perform more detailed setups and overrides for each individual test case or suite, ensuring mocks are configured exactly as required.
Overriding inline mocks
Per test mocking makes it straightforward to change the test parameters of the mocked module or class but you can also override inline mocks.
If we were using the someModule
inline mock and we wanted to override the someFunction
function that we have defined inline, we would first import the someFunction
function and then use mockImplementation
against it:
import { someFunction } from "./some_module.js";
someFunction.mockImplementation(() => "custom value");
expect(someFunction()).toBe("custom value");
// Optional: Restore the original mock implementation after the test
someFunction.mockRestore();
Note: although we are importing someFunction
we are not actually importing the real function tha belongs to the module. Because Jest mocks all of its properties and methods with the inline syntax, we are actually just importing that which Jest has aready mocked, but the syntax is a bit misleading.
Applied to classes
The same approaches (with minor differences) can be used with classes:
Using inline (where the class is not the default export):
jest.mock("./SomeClass", () => {
return {
SomeClass: jest.fn().mockImplementation(() => {
return {
someFunction: jest.fn(() => "value"),
someFunctionWithParam: jest.fn((param) => ({ property: param })),
someAsyncFunction: jest.fn(() => Promise.resolve("value")),
someOtherFunctionThatResolves: jest.fn().mockResolvedValue("some data"),
};
}),
};
});
Using per test:
import SomeClass from "./someClass";
jest.mock("./someClass");
let someClassMock = {
someFunction: jest.fn(() => "value"),
someFunctionWithParam: jest.fn((param) => ({ property: param })),
someAsyncFunction: jest.fn(() => Promise.resolve("value")),
};
// Mock class implementation
SomeClass.mockImplementation(() => someClassMock);
it("should do something", () => {
const newValue = "new value";
someClassMock.someFunction.mockReturnValue(newValue);
});
Check that a function has been called within another function
function toBeCalledFunction() {
console.log("Original function called");
}
function callerFunction() {
toBeCalledFunction();
}
test("spy on toBeCalledFunction", () => {
const spy = jest.spyOn(global, "toBeCalledFunction"); // Replace `global` with the appropriate object/context if the function is not global
callerFunction();
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // Restore the original function after spying
});
Mock a function that needs to resolve to something within another function
We have two functions, one that gets data and another that processes it. We want to mock the function that gets data and return a value that the processing function can use.
async function getData() {
// ... Fetch some data from an API or database
return fetchedData;
}
async function processData() {
const data = await getData();
// ... Process the data
return processedData;
}
The mocking part:
const mockData = { key: "value" }; // Mocked data
jest.mock("./path-to-file-where-getData-is", () => ({
getData: jest.fn().mockResolvedValue(mockData),
}));
test("test processData function", async () => {
const result = await processData();
// Now, result contains the processed version of mockData
expect(result).toEqual(/* expected processed data based on mockData */);
});
We could also combine the above with a spy to check that the getData
function was called:
const getDataSpy = jest
.spyOn(moduleContainingGetData, "getData")
.mockResolvedValue(mockData);
const result = await processData();
expect(getDataSpy).toHaveBeenCalled();
expect(result).toEqual(/* expected processed data based on mockData */);
getDataSpy.mockRestore();
Mock a function that takes arguments
function addPrefix(str) {
return `prefix-${str}`;
}
test("dynamic mock for addPrefix function", () => {
const mockFunction = jest.fn((str) => `mock-${str}`);
// Example usage of mockFunction
const result1 = mockFunction("test");
const result2 = mockFunction("example");
expect(result1).toBe("mock-test");
expect(result2).toBe("mock-example");
});
Mocking network requests
Mocking Axios
jest.mock("axios", () => ({
get: jest.fn().mockResolvedValue(mockData),
post: jest.fn().mockResolvedValue(mockData),
}));
Or we could implement this way:
jest.mock("axios");
axios.get.mockResolvedValue({ data: "mockedData" });
axios.post.mockResolvedValue({ data: "mockedData" });
Then we can use the mocked axios functions in our tests:
const result = await fetchData(); // the function that uses Axios `get``
expect(result).toBe("mockedGetData");
const result = await sendData({ key: "value" }); // the function tha uses Axios `post`
expect(result).toBe("mockedPostData");
mockImplementation
For more configurable cases we can use mockImplementation
:
it("sends data", async () => {
// Mock axios.post using mockImplementation
axios.post.mockImplementation((url, data) => {
if (data.key === "value") {
return Promise.resolve({ data: "mockedPostData" });
} else {
return Promise.reject({ error: "An error occurred" });
}
});
const result = await sendData({ key: "value" });
expect(result).toBe("mockedPostData");
});
If we want to change the get
and post
values in different tests, we can do so by using mockImplementation
:
Mocking exceptions
Again we use mockImplementation
:
Say we have the following function:
// fetchData.js
import axios from "axios";
const fetchData = async (url) => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
throw new Error("Error fetching data");
}
};
export default fetchData;
We would mock the success and the error as follows:
import axios from "axios";
import fetchData from "./fetchData";
jest.mock("axios");
describe("fetchData", () => {
it("fetches data successfully", async () => {
axios.get.mockResolvedValue({ data: "mockedData" });
const result = await fetchData("https://api.example.com/data");
expect(result).toBe("mockedData");
});
it("throws an error when fetching fails", async () => {
axios.get.mockImplementation(() => {
throw new Error("API error");
});
// We use an asynchronous assertion here because we're expecting a promise to reject
await expect(fetchData("https://api.example.com/data")).rejects.toThrow(
"Error fetching data"
);
});
});
Parameterization
The following offers a good opportunity for parameterisation:
it("should return page for deletion from `ipages-live`", async () => {
// preview = false, isInternal = false
await deletePageFromS3("url", false, false);
const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
expect(deleteObjectCommand.input).toEqual({
Bucket: "bbc-ise-ipages-live",
Key: "url/index.html",
});
});
it("should return page for deletion from `preview`", async () => {
// preview = true, isInternal = false
await deletePageFromS3("url", true, false);
const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
expect(deleteObjectCommand.input).toEqual({
Bucket: "staff.bbc.com-preview",
Key: "preview/url/index.html",
});
});
...
Each time we are passing in three parameters to the deletePageFromS3
function which is the object under test. Each time there are different variations in the object that is output.
To parameterize the process rather than use repeated it
blocks we can combine the input paramters and outputs into an array:
const testParams = [
{
preview: false,
isInternal: false,
bucket: "ipages-live",
key: "url/index.html",
},
{
preview: true,
isInternal: false,
bucket: "staff.com-preview",
key: "preview/url/index.html",
},
];
Then use it.each
to loop through all possible parameter combinations:
it.each(testParams)(
"should return page for deletion from %s",
async ({ preview, isInternal, bucket, key }) => {
await deletePageFromS3("url", preview, isInternal);
const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
expect(deleteObjectCommand.input).toEqual({
Bucket: bucket,
Key: key,
});
}
);
This uses the %s
variable to print the parameters from each test, which outputs:
✓ should return page for deletion from {
preview: false,
isInternal: false,
bucket: 'ipages-live',
key: 'url/index.html'
} (1 ms)
✓ should return page for deletion from {
preview: true,
isInternal: false,
bucket: 'staff.com-preview',
key: 'preview/url/index.html'
}