Mocks and Spies with Jest

qmenoret

Quentin Ménoret

Posted on December 31, 2020

Mocks and Spies with Jest

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}`)
}
Enter fullscreen mode Exit fullscreen mode

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')
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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')
  })
}
Enter fullscreen mode Exit fullscreen mode

There are two things to remember about spies:

  • You still need to tell Jest to forget about the mock between tests using mockClear, mockReset or mockRestore (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')
  })
}
Enter fullscreen mode Exit fullscreen mode

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 called spyOn on the function. All mocked implementation or return value will be forgotten. Of course it also implies everything mockClear 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' }
})
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
qmenoret
Quentin Ménoret

Posted on December 31, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related