DEV Community

Cover image for Mock Javascript function within the module
ravi
ravi

Posted on

Mock Javascript function within the module

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 }
Enter fullscreen mode Exit fullscreen mode
// cli.js
function checkCommandLineArgs() {
    // logic to parse commandline arguments and return
    // input and output
}
module.exports = { checkCommandLineArgs }
Enter fullscreen mode Exit fullscreen mode

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.

  1. Mock checkCommandLineArgs to return input and output files.
  2. Mock process() to avoid the call to time consuming operation.
  3. Spy on log method of console module.
  4. 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');
    })
Enter fullscreen mode Exit fullscreen mode

If you run the test, you will notice

  1. Actual process() method is called which is not expected.
  2. 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;

Enter fullscreen mode Exit fullscreen mode

Solutions

There are multiple ways to solve this problem

  1. Move process to separate module
    If we create process.js and move the process() 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 external Api, libraries, databases etc. More commonly, we implement such modules in service layer.

  2. Use the exported process function
    In the second approach, we can use the exported process() 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 referencing myModule.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 }
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
    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');
    })
Enter fullscreen mode Exit fullscreen mode

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)