Mocking
Mocking is a technique used to replace certain parts of a system under test with simplified, controllable, and predictable objects. These mock objects mimic the behavior of real objects but do not perform any actual operations. Instead, they are programmed to return predefined responses or simulate specific scenarios.
Some of the key benefits of the mocking are :-
- Isolation
- Faster Testing
- Test Scenarios
- Reliable Testing
- Decoupling
In this post, I am not going to cover the basics of how to use mocks or using Jest in general. Instead, I will cover one specific use case that you might come across while writing unit tests. Here is the link to the Jest documentation if you want to refer.
Problem Statement
Let's say I have a begin()
function in index.js
. It calls two other functions checkCommandLineArgs()
and process()
. While checkCommandLineArgs
is the imported function from cli.js
, process()
is function within index.js module. Also, process()
is a time consuming IO operation.
// index.js
async function process(inputFile, outputFile) {
// I/O operation
}
async function begin() {
try {
const { input, output } = cli.checkCommandLineArgs();
await process(input, output)
console.log("Completed");
return "Completed";
}
catch (err) {
console.error("Error : ", err);
}
}
module.exports = { begin, process }
// cli.js
function checkCommandLineArgs() {
// logic to parse commandline arguments and return
// input and output
}
module.exports = { checkCommandLineArgs }
Our objective is to verify that begin()
returns Completed
when there is no validation error and process()
executes without any error. We need to do this without calling the real implementation of process()
and checkCommandLineArgs()
. For that, our unit test have following setup.
- Mock checkCommandLineArgs to return input and output files.
- Mock process() to avoid the call to time consuming operation.
- Spy on log method of
console
module. - Assert "Completed" is returned.
const index = require('../src/index');
const cli = require("../src/cli")
it("Begin method should return Completed on valid inputs", async () => {
const mock = jest.fn();
mock.mockReturnValue(["input.txt", "output.txt"]);
cli.checkCommandLineArgs = mock;
const processMock = jest.fn();
index.process = processMock;
const logSpy = jest.spyOn(console, 'log');
// Act
const result = await index.begin();
//Assert
expect(processMock).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith('Completed');
expect(result).toEqual('Completed');
})
If you run the test, you will notice
- Actual process() method is called which is not expected.
- Since process mock is not called, the test will fail.
What is the issue
If we look at test code, we are mocking the process
function of the index module. On the other hand, though begin
function is using the same process
function, it's reference is not same as exported process
function. Due to this, mock is not able to replace the real implementation.
const processMock = jest.fn();
index.process = processMock;
Solutions
There are multiple ways to solve this problem
Move process to separate module
If we createprocess.js
and move theprocess()
to it and import it in the index.js and index.test.js then the mock will work correctly.
Besides solving this issue, it is important as a developer to analyze if the functions are organized in appropriate modules.
In our case,process()
is dependent on IO operations which most likely would depend on some external libraries. So it is better to keep function in separate modules that depend on externalApi
,libraries
,databases
etc. More commonly, we implement such modules in service layer.Use the exported process function
In the second approach, we can use the exportedprocess()
in the begin function. What it does is we end up using the same process function in unit test and the actual method. As you can see, I am referencingmyModule.process
.
// index.js
async function begin() {
try {
const { input, output } = cli.checkCommandLineArgs();
await myModule.process(input, output)
console.log("Completed");
return "Completed";
}
catch (err) {
console.error("Error : ", err);
}
}
const myModule = { begin,process }
For this, earlier unit test will work as expected.
3.Passing function as dependency
In this approach, we pass the process function as the parameter of the begin function. This is more common in object oriented languages like Java and C#. It is commonly known as dependency injection. This require change in the function definition which few people would not like. I personally find it less natural with javascript. But it definitely works
async function begin(process) {
try {
const { input, output } = cli.checkCommandLineArgs();
await process(input, output)
console.log("Completed");
return "Completed";
}
catch (err) {
console.error("Error : ", err);
}
}
it("Begin method should return Completed on valid inputs", async () => {
const mock = jest.fn();
mock.mockReturnValue(["input.txt", "output.txt"]);
cli.checkCommandLineArgs = mock;
const logSpy = jest.spyOn(console, 'log');
const processSpy = jest.fn();
// Act
const result = await begin(processSpy);
//Assert
expect(processSpy).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith('Completed');
expect(result).toEqual('Completed');
})
Hope this post helps to clear out the problem and explain the solutios. Feel free to drop in questions and feedback if you have any.
Top comments (0)