How To Mock Only One Function From A Module In Jest

How To Mock Only One Function From A Module In Jest

Mocking dependencies of your code is one of the fundamental aspects to software testing that allows developers to gain control over data flows and behaviour of the code. As a JavaScript testing framework, Jest has an extensive collections of APIs that will make our lives easier and help with mocking dependencies. However, sometimes there are so much options that it's hard to know all of them, let alone determine the most optimal one.

I had a similar case where I was importing several different exports from a module @module/api and using it throughout my code. In my tests however, I wanted to mock one particular imported function functionToMock and leave all the other imports intact. The particular function was performing some logic in the background that couldn't be mimicked in a test environment and also just wasn't meaningful for the integrity of my tests. So I wanted to mock it away, but it was important that all the other imports would still work as how the end-users would experience it.

After doing quite some research and trying out different approaches, I learned quite a bit about the different available mocking approaches, the differences between them, and in general a better understanding of mocking dependencies in Jest. In this article, I will share my learnings on how to mock one particular function from an imported module in Jest.

Manual Mocking

The main thing I found out after the entire process is that trying to mock one particular function from an imported module is fundamentally the same as mocking any function from any other module. So it makes sense to start out with the most fundamental approach, namely manually mocking the function.

import * as moduleApi from '@module/api';

// Somewhere in your test case or test suite
moduleApi.functionToMock = jest.fn().mockReturnValue({ someObjectProperty: 42 });

What we're doing here is first importing all the imports from @module/api, bundling it into an object, and storing it into the variable called moduleApi. Then, we're overwriting the function that we want to mock functionToMock with a Jest mock function. This means that inside of our test environment, any calls to functionToMock from our code will not trigger the actual function but rather this jest mock function. After this, we can use the Jest utility functions to alter the behaviour of this function based on the requirements of a test or test suite. In the above example, we used the mockReturnValue to make the mock function always return a certain value, which in this case is an object with a certain key and value.

This is the most low level approach and should work in most of the use cases. The other approaches basically use Jest utility functions that are basically an abstraction in some form of this fundamental approach. However, manually mocking is quite tedious and requires manual bookkeeping when dealing with more complex situations. Hence, this approach is probably best used as a fallback after trying out the built-in utility functions from Jest.

There are also certain cases where this approach doesn't work. The error that I encountered the most when trying this approach was TypeError: Cannot set property functionToMock of #<Object> which has only a getter. In that case, you can try one of the other approaches in this article.

Spying on the function using jest.spyOn

Another approach to mock a particular function from an imported module is to use the jest.spyOn function. The API for this function seems to be exactly what we need for our use case, as it accepts an entire module and the particular export that should be spied on.

import * as moduleApi from '@module/api';

// Somewhere in your test case or test suite
jest.spyOn(moduleApi, 'functionToMock').mockReturnValue({ someObjectProperty: 42 });

Usage wise it's basically the same as manually mocking it as described in the previous section. But this is slightly cleaner syntax, allows for easier cleanup of the mocks, and makes performing assertions on the function easier since the jest.spyOn will return the mocked function. But functionality wise for this use case there is no difference between spying on the function using this code or manually mocking it.

However, from a technical perspective there is quite a difference because jest.spyOn(moduleApi, 'functionToMock') on its own will still run the actual functionToMock code rather than mocking it. Spying a function from a module will only keep track of its calls. If you also want to mock away the underlying code, you will have to chain it with the usual mock utility functions like mockReturnValue or mockImplementation.

Using this approach, there's the chance that you will run across a TypeError: Cannot redefine property: functionToMock at Function.defineProperty (<anonymous>). This is similar to the error that we faced when trying to manual mock. Still I would suggest you to first try doing manual mocking to solve the issue if you haven't already, since the overhead is not that large. But if both manual mocking and spying on the function does not work out, you can refer to the next and final approach.

Mock the entire module and restore unnecessary mocks using jest.requireActual

In most cases, one of the other approaches should do the trick and satisfy your use case. But in rare cases you'll run into errors that prevent you from redefining the single exported function. This is exactly what I faced as well and the solution that I used is as follows.

import { functionToMock } from "@module/api"; // Step 3.

// Step 1.
jest.mock("@module/api", () => {
    const original = jest.requireActual("@module/api"); // Step 2.
    return {
        ...original,
        functionToMock: jest.fn()
    };
});

// Step 4. Inside of your test suite:
functionToMock.mockImplementation(() => ({ mockedValue: 2 }));

There is a lot going on here, so let's break it down.

In step 1, we use jest.mock("@module/api", ...) to mock the entire module. This means that every import from the module will be a mocked function in the test environment. This is obviously not what we want since we only want to mock the functionToMock export. We can address this in the second argument of the jest.mock call, which accepts a callback that should return an object. This object is returned instead of the actual module when the module is imported in any way in our test environment.

Then in step 2, inside the second argument callback, we use jest.requireActual("@module/api") to capture the original code and imports from the module and store it in a variable. Then, we create the object that should replace the module's imports by doing two things: put all the original imports into it and override the functionToMock that we want to mock with a jest mocking function.

Then to use the mocked function, we have to import the function from the module, step 3. Lastly somewhere inside your test suite, step 4, you can use that import to do various things like customising the mock implementation as is shown in the above example code, or performing assertions onto it.

What we've basically done is mock the entire module, create a snapshot of the actual imports of the module, use that snapshot as the mocked version, and then tweak any import as we like for our test environment by overriding it in the mocked module. In this case we only wanted to mock the functionToMock function, so we only had to override that with a jest mock function.

Due to the "throw away everything and start from scratch" nature of this approach, it is best served as a last resort solution when trying to mock one particular function from a module in Jest. While this approach will work in all cases, it's quite an overkill solution for what we're trying to achieve and can cause quite some confusion down the road for other developers. If possible, try to use the more sophisticated approach of spying on the export or even manually mocking it. But if all else fails or the other two approaches don't work out, this approach will solve your problem.

Did you find this article valuable?

Support Chak Shun Yu's Blog by becoming a sponsor. Any amount is appreciated!