Two shades of mocking a function in Vitest
Tags: testing, javascript, vitest, tutorial
Published: 2024-02-20 12:23:27

---
title: "Two shades of mocking a function in Vitest"
description: "How to mock a dependency for your unit tests in Vitest using vi.fn() and vi.spyOn() APIs, and when to use which."
cover_image: "https://res.cloudinary.com/mayashavin/image/upload/v1708431140/articles/testing/two_way_of_testing.png"
published: true
canonical_url: https://mayashavin.com/articles/two-shades-of-mocking-vitest
series: Unit testings with Vitest Series
tags: ['Testing', 'JavaScript', 'Vitest', 'tutorial']
---

_Mocking is the approach of replacing a real object with a fake object that simulates the original behavior, hence allowing us to test our code in isolation (unit test), without having to worry about instability may arised by the behavior of the external dependencies. In this article, we will discuss two different ways of mocking a external function call in Vitest, using `vi.fn()` and `vi.spyOn()` APIs, and when to use which._

## Table of contents

- [Table of contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Our test scenario](#our-test-scenario)
- [Mocking a function using vi.fn()](#mocking-a-function-using-vifn)
- [Mocking a global function using vi.spyOn()](#mocking-a-global-function-using-vispyon)
- [vi.fn vs vi.spyOn - When to use which?](#vifn-vs-vispyon---when-to-use-which)
- [Summary](#summary)

## Prerequisites

You should have a Vite project set up and running. The recommended way is to initialize your project with Vite as its bundle management tool, using the command `npm create vite@latest`.

You need to install [Vitest](https://vitest.dev/) as the unit testing framework for our tests, by running the following command:

```bash
yarn add -D vitest
```

Additionally, if you are testing your front-end application, you can install the following tools to help you with any framework-specific unit tests:

- [JSDOM](https://github.com/jsdom/jsdom) as the DOM environment for running component unit tests.
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) as the React testing utilities or [Vue Test Utils](https://test-utils.vuejs.org/guide/) for Vue applications.

In `vitest.config.js` (or `vite.config.js` for Vite projects), we add the following `test` object to enable Vitest APIs as globally available in our tests:

```js
//...
export default defineConfig({
  test: {
    global: true,
    environment: 'jsdom', //this is optional, only when you are testing DOM related functionalities
  },
})
```

If you are writing your tests in TypeScript, you can also add the following `tsconfig.json` file to provide the types for Vitest APIs to TypeScript type checker:

```json
{/** tsconfig.json */
  "compilerOptions": {
    "types": [["vitest/globals"]]
  }
}
```

Lastly, we add a new `test:unit` command to the `package.json` file. This command will find and run the unit tests from the `src/` as its root folder, as show below:

```json
"scripts": {
    "test:unit": "vitest --root src/",
}
```

With this setup, we are ready to explore different ways of mocking external function call with Vitest, starting with our testing scenario.

## Our test scenario

We have a function `fetchData` that makes a network request to an API and returns the data, as follows:

```js
// fetchData.js
export async function fetchData() {
  const response = await fetch(
    'http://exploringvue.com/.netlify/functions/pizzas'
  );
  const data = await response.json();
  return data;
}
```

We want to test this function, and we need to mock the `fetch` call to avoid making a real network request, potentially harming the quality and the scope of the test scenarios.

There are many ways of mocking a `fetch` call in Vitest. One way is to use `vi.fn()`, which we will discuss next.

## Mocking a function using vi.fn()

`fetch` is a global method that helps to retrieve resources asynchronously from the server. It is available in both server side (Nodejs API) and client side (Web API) environments. Our testing environment is Nodejs, hence the `fetch` method is available as part of `global` object, as `global.fetch` and **not** `window.fetch`.

`vi.fn()` is the most straightforward approach to mock a function call in Vitest. It returns a new mock instance (or spy instance) that we can use to replace the original function, as shown below:

```js
import { vi } from 'vitest';
import { fetchData } from './fetchData';

const mockedImplementation = () => Promise.resolve({ 
  json() { 
    return { data: 'mocked data'}
  } 
})

describe ('fetchData', () => {
  global.fetch = vi.fn(mockedImplementation);

  it('should return the mocked data', async () => {
    const data = await fetchData();
    expect(data).toEqual({ data: 'mocked data' });
  });
});
```

In the above code, we are replacing the original `global.fetch` method with a new mock function that returns a resolved promise with the data `{ data: 'mocked data' }`. This way, we can test the `fetchData` function without making a real network request.

`vi.fn()` also accepts no arguments, which means it returns a new mock function that does nothing, such as `global.fetch = vi.fn()`, and uses `mockImplementation()` to provide a new mock implementation:

```js
describe ('fetchData', () => {
  global.fetch = vi.fn();
  global.fetch.mockImplementation(mockedImplementation);
  //...
});
```

This approach can be handy when we only want to fast-mock a function call's return value without any side effects to the test results.

However, there is a problem with the mock approach with `vi.fn()`. In our current code, we are losing the original implementation of the `global.fetch` method, and we won't be able to restore it after the test. This can potentially cause test-contamination, where the mock function from one test may affect the result of another test, which uses the original `fetch`. To solve this issue, we should save the original implementation of the `global.fetch` method before all the tests run within the same test suite:

```js
import { vi } from 'vitest';

describe ('fetchData', () => {
  let originalFetch;

  beforeAll(() => {
    originalFetch = global.fetch;
    global.fetch = vi.fn(mockedImplementation);
  });

  // ...tests
});
```

And after running all the test cases, we restore the `global.fetch` method to its original implementation:

```js
describe ('fetchData', () => {
  // ...

  afterAll(() => {
    global.fetch = originalFetch;
  });
});
```

The above code will ensure the `global.fetch` function will be restored after we finish with the `fetchData` test suite and won't affect other relevant tests. Altenatively, there is a better way to mock and spy a function while maintains the original implementation and reduces the chance for test-contamination, with lesser code. We will discuss this in the next section.

## Mocking a global function using vi.spyOn()

Similar to `vi.fn()`, `vi.spyOn()` also return a Vitest spy instance but we don't need to replace the original `global.fetch` with it. `vi.spyOn()` accepts two arguments:

- The object containing the method (`global`), and
- The method name to spy on (`fetch`).

```js
describe ('fetchData', () => {
  const fetchSpy = vi.spyOn(global, 'fetch');

  // ...tests
});
```

And then we can mock the implementation of the `fetch` method using the `mockImplementation` method of the spy instance:

```js
describe ('fetchData', () => {
  const fetchSpy = vi.spyOn(global, 'fetch');

  fetchSpy.mockImplementation(mockedImplementation);

  // the tests are the same
});
```

With this approach, we don't need to save the original implementation of the `global.fetch` method manually. Instead, we can use the `mockRestore` method of the spy instance within `afterAll` hook to restore the original implementation afterwards:

```js
describe ('fetchData', () => {
  //...

  afterAll(() => {
    fetchSpy.mockRestore();
  });
});
```

This is my preferred way of mocking a external function call in Vitest, as it is more readable and maintainable, in comparison to the `vi.fn()` approach. In fact, `vi.spyOn()` returns a spy instance whose implementation defaults to the original method, and we can change it per test case. This approach is really handy for both integrating tests and unit tests, as it allows us to spy on a specific dependency of the target code, and to assert the interaction between them.

So, when should we use `vi.fn()` and `vi.spyOn()`?

## vi.fn vs vi.spyOn - When to use which?

While both `vi.fn()` and `vi.spyOn()` can be used to mock a function call in Vitest, and behind the scenes, `vi.spyOn` is a wrapper around `vi.fn`, they are designated for different use cases, as shown in the table below:

| `vi.fn()` | `vi.spyOn()` |
| --- | --- |
| When you want to replace the original function with a new mock function | When you want to spy on the original function and mock its implementation per scenario |
| You need to save the original implementation  manually | There is a built-in method for restore the original function |
| More suitable for fast-mocking a function call | More suitable for mocking a function call with a side effect |
| For unit testing the function's implementation, not its interaction with other functions | For both unit and integration tests |

## Summary

We have learned how to mock a function call, such as the global `fetch` in Vitest using `vi.fn()` and `vi.spyOn()`. We have also discussed the differences between these two approaches and when to use which. Mocking is a powerful tool for isolating the dependencies of the code we are testing, or for asserting the interaction between them. In the next article, we will discuss further on different mocking scenarios, both for unit and integration tests, and the best practices for asserting mocks and cleaning up after mocking.

👉 _Learn about Vue 3 and TypeScript with my new book [Learning Vue](https://www.oreilly.com/library/view/learning-vue/9781492098812/)!_

👉 _If you'd like to catch up with me sometimes, follow me on [X](https://x.com/MayaShavin) | [LinkedIn](https://www.linkedin.com/in/mayashavin)._

Like this post or find it helpful? Share it 👇🏼 😉