Override functions in individual tests using Jest

Sometimes you want to mock a function in some tests but not others. Sometimes you want to supply different mocks to different tests. Jest makes this tricky: its default behavior is to override a package’s function for a whole test file, not just a single test. This seems odd if you’ve used flexible tools like Python’s @patch or Laravel’s service container.

This post will show you how to mock functions for individual tests, then fallback to the original implementation if no mock was provided. Examples will be given for both CommonJS and ES modules. The techniques demonstrated in this post will work for both first-party modules and third-party packages.

CommonJS vs ES Modules

Since we’ll be covering multiple module systems in this post, it’s important to understand what they are.

CommonJS (abbreviated CJS) is the module system in Node.js. It exports functions using module.exports and imports functions using require():

// CommonJS export  function greet() { return "Hello, world!";
} module.exports = { greet };
Enter fullscreen mode Exit fullscreen mode
// CommonJS import const getUsersList = require('./greet');
Enter fullscreen mode Exit fullscreen mode

ES modules (abbreviated ESM) is the module system that’s used by the browser. It exports functions using the export keyword and imports functions using the import keyword:

// ES module export export default function greet() { return "Hello, world!";
}
Enter fullscreen mode Exit fullscreen mode
// ES module import import { greet } from "./greet";
Enter fullscreen mode Exit fullscreen mode

Most frontend JavaScript developers use ES modules at the time of writing this post, and many server-side JS devs use them as well. However, CommonJS is still the default for Node. Regardless of which system you use, it is worth reading the whole article to learn about Jest’s mocking system.

Mocking a single exported function with CommonJS

Typically a CommonJS file will export their modules using object syntax, like shown below:

// CommonJS export  function greet() { return "Hello, world!";
} module.exports = { greet: greet };
Enter fullscreen mode Exit fullscreen mode

However, it is also possible to export a function by itself:

// CommonJS export  function greet() { return "Hello, world!";
} module.exports = greet;
Enter fullscreen mode Exit fullscreen mode

I wouldn’t necessarily recommend doing this in your own code: exporting an object will give you fewer headaches while developing your application. However, it is common enough that it’s worth discussing how to mock a bare exported function in CommonJS, then fallback to the original if a test does not provide its own implementation.

Let’s say we have the following CommonJS file we’d like to mock during tests:

// cjsFunction.js function testFunc() { return "original";
} module.exports = testFunc;
Enter fullscreen mode Exit fullscreen mode

We could mock it in our tests using the following code:

const testFunc = require("./cjsFunction"); jest.mock("./cjsFunction"); beforeEach(() => { testFunc.mockImplementation(jest.requireActual("./cjsFunction"));
}); it("can override the implementation for a single test", () => { testFunc.mockImplementation(() => "mock implementation"); expect(testFunc()).toBe("mock implementation"); expect(testFunc.mock.calls).toHaveLength(1);
}); it("can override the return value for a single test", () => { testFunc.mockReturnValue("mock return value"); expect(testFunc()).toBe("mock return value"); expect(testFunc.mock.calls).toHaveLength(1);
}); it("returns the original implementation when no overrides exist", () => { expect(testFunc()).toBe("original"); expect(testFunc.mock.calls).toHaveLength(1);
});
Enter fullscreen mode Exit fullscreen mode

How it works

When we call jest.mock("./cjsFunction"), this replaces the module (the file and all of its exports) with an auto-mock (docs). When an auto-mock is called, it will return undefined. However, it will provide methods for overriding the mock’s implementation, return value, and more. You can see all the properties and methods it provides in the Jest Mock Functions documentation.

We can use the mock’s mockImplementation() method to automatically set the mock’s implementation to the original module’s implementation. Jest provides a jest.requireActual() method that will always load the original module, even if it is currently being mocked.

Mock implementations and return values are automatically cleared after each test, so we can pass a callback function to Jest’s beforeEach() function that sets the implementation of the mock to the original implementation before each test. Then any tests that wish to provide their own return value or implementation can do that manually within the test body.

Mocking CommonJS when exporting an object

Let’s say that the code above had exported an object instead of a single function:

// cjsModule.js function testFunc() { return "original";
} module.exports = { testFunc: testFunc,
};
Enter fullscreen mode Exit fullscreen mode

Our tests would then look like this:

const cjsModule = require("./cjsModule"); afterEach(() => { jest.restoreAllMocks();
}); it("can override the implementation for a single test", () => { jest .spyOn(cjsModule, "testFunc") .mockImplementation(() => "mock implementation"); expect(cjsModule.testFunc()).toBe("mock implementation"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
}); it("can override the return value for a single test", () => { jest.spyOn(cjsModule, "testFunc").mockReturnValue("mock return value"); expect(cjsModule.testFunc()).toBe("mock return value"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
}); it("returns the original implementation when no overrides exist", () => { expect(cjsModule.testFunc()).toBe("original");
}); it("can spy on calls while keeping the original implementation", () => { jest.spyOn(cjsModule, "testFunc"); expect(cjsModule.testFunc()).toBe("original"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
});
Enter fullscreen mode Exit fullscreen mode

How it works

The jest.spyOn() method allows Jest to record calls to a method on an object and provide its own replacement. This only works on objects, and we can use it because our module is exporting an object that contains our function.

The spyOn() method is a mock, so its state must be reset. The Jest spyOn() documentation recommends resetting the state using jest.restoreAllMocks() in an afterEach() callback, which is what we did above. If we did not do this, the mock would return undefined in the next test after spyOn() was called.

Mocking ES modules

ES modules can have default and named exports:

// esmModule.js export default function () { return "original default";
} export function named() { return "original named";
}
Enter fullscreen mode Exit fullscreen mode

Here’s what the tests for the file above would look like:

import * as esmModule from "./esmModule"; // Whether or not you need this block will depend 
// on what you're mocking.
jest.mock("./esmModule", () => { const originalModule = jest.requireActual("./esmModule"); return { __esModule: true, ...originalModule, };
}); afterEach(() => { jest.restoreAllMocks();
}); it("can override the implementation for a single test", () => { jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default"); jest .spyOn(esmModule, "named") .mockImplementation(() => "mock implementation named"); expect(esmModule.default()).toBe("mock implementation default"); expect(esmModule.named()).toBe("mock implementation named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1);
}); it("can override the return value for a single test", () => { jest.spyOn(esmModule, "default").mockReturnValue("mock return value default"); jest.spyOn(esmModule, "named").mockReturnValue("mock return value named"); expect(esmModule.default()).toBe("mock return value default"); expect(esmModule.named()).toBe("mock return value named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1);
}); it("returns the original implementation when no overrides exist", () => { expect(esmModule.default()).toBe("original default"); expect(esmModule.named()).toBe("original named");
});
Enter fullscreen mode Exit fullscreen mode

How it works

This looks almost the same as the previous CommonJS examples, with a few key differences.

First, we’re importing our module as a namespace import.

import * as esmModule from "./esmModule";
Enter fullscreen mode Exit fullscreen mode

This allows us to bundle the default function and all of the named functions into a single object, which we need in order to be able to use jest.spyOn().

After that, we have the following bit of code:

jest.mock("./esmModule", () => { const originalModule = jest.requireActual("./esmModule"); return { __esModule: true, ...originalModule, };
});
Enter fullscreen mode Exit fullscreen mode

This code replaces the module with a Jest ES Module mock that contains all of the module’s original properties. Without this, jest.spyOn() doesn’t always work. This code may or may not be necessary in your project, depending on how Jest is configured. For my current project, it’s necessary to use a jest.mock() call like this for react-router-dom, but unnecessary for my own ES Modules.

So how will you know when you need to use jest.mock() when using ES modules? You’ll get a TypeError in your tests when you need it and don’t have it. When I don’t include the jest.mock() block above on react-router-dom and try to mock useNavigate() via spy-on, I get the following error:

 TypeError: Cannot redefine property: useNavigate at Function.defineProperty (<anonymous>)
Enter fullscreen mode Exit fullscreen mode

Finally, when we want to spy on the default export, we use "default" in our code:

 jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default");
Enter fullscreen mode Exit fullscreen mode

Wrapping up

I hope this information is valuable as you write your own tests. Not every app will benefit from being able to fallback to the default implementation when no implementation is provided in a test itself. Indeed, many apps will want to use the same mock for a whole testing file. However, the techniques shown in this post will give you fine-grained control over your mocking.

Let me know if I missed something or if there’s something that I didn’t include in this post that should be here.


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top