Mocks and Spies with Jest
Quentin Ménoret
Posted on December 31, 2020
When writing unit test, you often have to mock some functions. It can be to make your tests deterministic, or to assert that a specific function gets called. Let's imagine you are trying to assess that your function calls the right API using fetch.
async function getUser(id) {
return fetch(`/users/${id}`)
}
Let's see the different options we have when trying to assert this.
Using jest.fn
The first way to achieve it is to use directly jest.fn
and replace the fetch function.
describe('my test', () => {
it('calls the right route', async () => {
// Create an empty mock function which just returns nothing
const mockedFetch = jest.fn()
// Set the global fetch to be this function
global.fetch = mockedFetch
await getUser(id)
expect(mockedFetch).toHaveBeenCalledWith('/users/12')
})
}
This will work, but there are some drawbacks. The biggest one being: you will have to manually keep a reference to the actual fetch method, and put it back in place after the test. If this fails, it will impact all the other tests in your test suite.
You could do it this way:
describe('my test', () => {
const realFetch = global.fetch
beforeAll(() => {
global.fetch = jest.fn()
})
afterAll(() => {
global.fetch = realFetch
})
}
Using jest.spyOn
A better approach in this case would be to use a spy. Spies have all the features of a mock function, but leave you with more flexibility. More importantly, Jest will handle the cleaning of mocks for you. Here is what the test look like using spies:
describe('my test', () => {
it('calls the right route', async () => {
jest.spyOn(global, 'fetch')
await getUser(id)
expect(global.fetch).toHaveBeenCalledWith('/users/12')
})
}
There are two things to remember about spies:
- You still need to tell Jest to forget about the mock between tests using
mockClear
,mockReset
ormockRestore
(more on that later) - By default it just spies on the function and does not prevent the original code to be executed.
If we wanted to fix these 2 behaviours, the test would look like this:
describe('my test', () => {
beforeEach(() => {
jest.restoreAllMocks()
})
it('calls the right route', async () => {
jest.spyOn(global, 'fetch').mockReturnValue({})
await getUser(id)
expect(global.fetch).toHaveBeenCalledWith('/users/12')
})
}
Here we prevented the actual call to fetch using mockReturnValue
(mockImplementation
can be used too), and we restore all existing mocks to their initial state before every test run.
Clear, reset and restore
When clearing mocks, you have 3 possible functions you can call:
-
mockClear
- clearing a mock means clearing the history of calls that already have been stored in the mock. It can be useful if you want to start counting calls after some point in your test. -
mockReset
- reseting a mock returns the mock to a fresh state, just like if you just calledspyOn
on the function. All mocked implementation or return value will be forgotten. Of course it also implies everythingmockClear
implies. -
mockRestore
- Restoring the function actually removes the mock, and restore the original implementation.
All these functions can be used in two different ways:
- Directly on a mocked function:
myMockedFunction.mockClear
- Globally to affect all existing mocks you've created:
jest. clearAllMocks()
Using jest.mock
Another approach to mocking with Jest is to use jest.mock
. It allows you to mock entirely a module. For instance:
// Here I am mocking the 'os' module entirely
// It now only exposes one function: hostname
// Which always returns 'my-computer'
jest.mock('os', () => {
return { hostname: () => 'my-computer' }
})
As you can see, you can use the second parameter to provide a factory so that the import return something. If you don't importing the module will just return an empty object.
It is also possible to write this factory in a specific file. For instance if you were to import the file src/my-module.js
, and wanted a specific factory for it in every test, you can create a file named src/__mocks__/my-module.js
. Whatever this file exports if what will be imported when calling jest.mock('src/my-module')
without a factory provided.
So what do I use now?
As much as possible, try to go with the spyOn
version.
Using jest.fn
directly have a few use cases, for instance when passing a mocked callback to a function.
jest.mock
is powerful, but I mostly use it to prevent loading a specific module (like something that needs binaries extensions, or produces side effects). I also use it when I need to mock a lot of functions from an external module at once.
Photo by Tobias Tullius on Unsplash
Posted on December 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.