Writing unit tests for code with dependencies can be difficult. This is especially true for large code bases where it's tricky to come up with fixtures that will cover all the cases we need to test.
But what if we could control the return value of a function dependency, no matter what arguments it is called with?
This is where mock functions come in.
Mock functions are a testing tool that allows us to track how function dependencies are called and control their return values. This makes it possible for us to manipulate the control flow of the tested program and reach even those difficult-to-reproduce edge-cases when writing tests.
This article will provide an introduction into the concepts behind mocking and how it relates to unit testing. We will learn how to mock functions and imported function modules with Jest, and write tests that rely on those mocks to increase the coverage of our test cases.
We will assume that we're testing a couple of validation rules:
We want to see what our tests will teach us about the flaws in our code by passing and failing test cases. Fixing the implementation is not covered by this article, but feel free to play with it as we move through the article.
Read on to find out more!
To mock an imported function with Jest we use the jest.mock()function.
jest.mock() is called with one required argument - the import path of the module we're mocking. It can also be called with an optional second argument - the factory function for the mock. If the factory function is not provided, Jest will automock the imported module.
💡Note
Jest automock is the automatic mocking of imported modules with surface-level replacement implementations. Automocking is disabled by default since Jest 15, but can be enabled by configuring Jest with the automockflag.
When testing isAtLeast18() we have to keep in mind that the isInteger() dependency affects the module's behaviour:
if isInteger() is false , isAtLeast18() is false;
if isInteger() is true , isAtLeast18() depends on the value argument.
We'll start by testing the case of isInteger() returning false.
The isInteger.js module has a single default export - the isInteger() function. We will mock the imported module with a factory function that behaves just like the default export and returns a function. That function, when called, will always return false.
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");// The mock factory returns the function () => falsejest.mock("./isInteger",()=>()=>false);describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{// Should pass, but fails because of the isInteger() mockexpect(isAtLeast18(123)).toBe(false);// Should fail either wayexpect(isAtLeast18("abc")).toBe(false);Enter fullscreen modeExit fullscreen mode
💡Note
The import path of the mocked module must match the import path that is present in the module we're testing. The isAtLeast18.js module imports the isInteger.js module under the path "./isInteger". This is why our mock import path is also "./isInteger".
isAtLeast18() will now always return false no matter what we call it with, because the isInteger() mock is set to always return false.
But what about the case when isInteger() returns true?
To mock different return values depending on the test we will create a mock function.
💡Note
This unit test is a solitary unit test because the tested unit is isolated from its dependencies. Read more about solitary unit tests in the previous article: How to write unit tests in JavaScript with Jest.
A mock function is a function that replaces the actual implementation of a function with a "fake" (mock) implementation.
Mock functions track how they are called by external code. With a mock function we can know the number of times the function was called, the arguments it was called with, the result it returned, and more. This ability to "spy" on function calls is why mock functions are also called spies.
We use mock functions to override original function behaviour with custom mock implementations. Mock implementations help us control the return values of a function. This makes our tests more predictable (deterministic) and easier to write.
To mock a function with Jest we use the jest.fn()function.
jest.fn() can be called with an implementation function as an optional argument. If an implementation is provided, calling the mock function will call the implementation and return it's return value.
If no implementation is provided, calling the mock returns undefined because the return value is not defined.
// Without implementation, this mock returns `undefined`.constmockUndefined=jest.fn();// With implementation, this mock returns `true`.constmockTrue=jest.fn(()=>true).Enter fullscreen modeExit fullscreen mode
constmockOne=jest.fn(()=>false);// Example error: expect(jest.fn()).toHaveBeenCalledWith(...expected)constmockTwo=jest.fn(()=>false).mockName('mockTwo');// Example error: expect(mockTwo).toHaveBeenCalledWith(...expected)Enter fullscreen modeExit fullscreen mode
To change the mock implementation of a function with Jest we use the mockImplementation()method of the mocked function.
The mockImplementation() method is called with the new implementation as its argument. The new implementation will then be used in place of the previous one when the mock is called.
// The initial mock is a function that returns `true`.constmyMock=jest.fn(()=>true);// The new mock implementation has the function return `false`.myMock.mockImplementation(()=>false);Enter fullscreen modeExit fullscreen mode
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");constisInteger=require("./isInteger");// The mock factory returns a mocked functionjest.mock("./isInteger",()=>jest.fn());describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{// For this test we'll mock isInteger to return `false`isInteger.mockImplementation(()=>false);expect(isAtLeast18(123)).toBe(false);expect(isAtLeast18("abc")).toBe(false);it("passes if value is recognised as integer and is at least 18",()=>{// For this test we'll mock isInteger to return `true`isInteger.mockImplementation(()=>true);expect(isAtLeast18(123)).toBe(true);expect(isAtLeast18("abc")).toBe(false);Enter fullscreen modeExit fullscreen mode
💡Note jest.mock() works by modifying the Node module cache to give us the mock instead of the original implementation whenever we import a mocked module in a test file. To support ES module imports - where import statements have to come first in a file - Jest automatically hoistsjest.mock() calls to the top of the module. Read more about this technique here.
To check if a function was called correctly with Jest we use the expect()function with specific matcher methods to create an assertion.
We can use the toHaveBeenCalledWith()matcher method to assert the arguments the mocked function has been called with.
To assert how many times the mocked function has been called so far, we can use the toHaveBeenCalledTimes()matcher method.
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");constisInteger=require("./isInteger");jest.
mock("./isInteger",()=>jest.fn());describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{isInteger.mockImplementation(()=>false);expect(isAtLeast18(123)).toBe(false);// We expect isInteger to be called with 123expect(isInteger).toHaveBeenCalledWith(123);// We expect isInteger to be called onceexpect(isInteger).toHaveBeenCalledTimes(1);Enter fullscreen modeExit fullscreen mode
💡Note
While these are the most common matcher methods for functions, there are more matcher methods available in the Jest API docs.
Jest tracks all calls to mocked functions. A mocked function will remember the arguments and times it has been called, as well as the results of those calls.
When reusing mocked functions between tests it is useful to reset their states before running new tests to get a clear baseline. We can do that by clearing mocked functions between tests.
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");constisInteger=require("./isInteger");jest.mock("./isInteger",()=>jest.fn());describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{isInteger.mockImplementation(()=>false);expect(isAtLeast18(123)).toBe(false);expect(isInteger).toHaveBeenCalledWith(123);expect(isInteger).toHaveBeenCalledTimes(1);// Clear the mock so the next test starts with fresh dataisInteger.mockClear();it("passes if value is recognised as integer and is at least 18",()=>{isInteger.mockImplementation(()=>true);expect(isAtLeast18(123)).toBe(true);expect(isInteger).toHaveBeenCalledWith(123);// Without clearing, there would be 2 calls total at this pointexpect(isInteger).toHaveBeenCalledTimes(1);Enter fullscreen modeExit fullscreen mode
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");constisInteger=require("./isInteger");jest.mock("./isInteger",()=>jest.fn());// Clear mock data before each testbeforeEach(()=>{isInteger.mockClear();describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{isInteger.mockImplementation(()=>false);expect(isAtLeast18(123)).toBe(false);expect(isInteger).toHaveBeenCalledWith(123);expect(isInteger).toHaveBeenCalledTimes(1);it("passes if value is recognised as integer and is at least 18",()=>{isInteger.mockImplementation(()=>true);expect(isAtLeast18(123)).toBe(true);expect(isInteger).toHaveBeenCalledWith(123);expect(isInteger).toHaveBeenCalledTimes(1);Enter fullscreen modeExit fullscreen mode
💡Note
Jest provides four functions to hook into the set-up and tear-down process, both before and after each or all of the tests in a test file. These functions are: afterAll(), afterEach(), beforeAll(), beforeEach(). The afterAll() and beforeAll() variants are called only once for the entire test file. The afterEach() and beforeEach() variants are called once for every test in the test file.
To reuse mocks with Jest we create mocks in a __mocks__/subdirectory adjacent to the module we want to mock.
Mock files in the __mocks__/ subdirectory are used to automock the modules they are adjacent to when the module is mocked with jest.mock(). This is useful when dealing with a lot of repetition in setting up mocks such as when mocking common dependencies or configuration objects because it makes writing a mock factory function unnecessary.
Assuming a common configuration file that is used by many different modules, mocking it would look like this:
💡Note
Remember: mocks and automocking are only in effect when running tests with Jest. They do not have an effect on the code in development or production.
That's it! We're now ready to mock imported functions with Jest.
// isAtLeast18.spec.jsconstisAtLeast18=require("./isAtLeast18");constisInteger=require("./isInteger");// The mock factory returns a mocked functionjest.mock("./isInteger",()=>jest.fn());beforeEach(()=>{isInteger.mockClear();describe("isAtLeast18",()=>{it("fails if value is not recognised as integer",()=>{isInteger.mockImplementation(()=>false);expect(isAtLeast18(123)).toBe(false);expect(isInteger).toHaveBeenCalledWith(123);expect(isInteger).toHaveBeenCalledTimes(1);it("passes if value is recognised as integer and is at least 18",()=>{isInteger.mockImplementation(()=>true);expect(isAtLeast18(123)).toBe(true);expect(isInteger).toHaveBeenCalledWith(123);expect(isInteger).toHaveBeenCalledTimes(1);Enter fullscreen modeExit fullscreen mode
Write more comprehensive tests and use fixtures to cover any additional cases. If you've done your homework from the previous article, try continuing from where you left off.
Fix the code so any failed tests pass or write a newer, better implementation.
Achieve 100% code coverage in the coverage report.
Thank you for taking the time to read through this article!
Have you tried mocking imported functions with Jest before? What was your experience like?