Mock Javascript function within the module
Tags: jest, mock, javascript, node
Published: 2023-08-03 07:45:52


## 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](https://jestjs.io/docs/getting-started) 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.

```javascript
// 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 }
```

```javascript
// 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.
 
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.

```javascript
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

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.

```javascript
const processMock = jest.fn();
index.process = processMock;

```

### 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`.

```javascript
// 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

```javascript
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);
    }
}
```
```javascript
    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.