Module and environment variable stubbing for efficient testing in Vitest
Tags: testing, javascript, vitest, tutorial
Published: 2024-04-01 21:04:01

---
title: "Module and environment variable stubbing for efficient testing in Vitest"
description: "Learn how to mock a module and stub an environment variable in functional testing with Vitest"
published: true
tags: [Testing, JavaScript, Vitest, tutorial] 
cover_image: https://res.cloudinary.com/mayashavin/image/upload/v1712003825/articles/testing/mock_stub_vitest.jpg
canonical_url: "https://mayashavin.com/articles/mock-module-stub-variable-vitest"
series: Unit testings with Vitest Series
---

_In this article, we will continue discussing another approach of mocking, such as an external module, using `vi.mock()` and how we can stub different values for an environment variable using `vi.stubEnv()`._

## Table of contents

- [Table of contents](#table-of-contents)
- [Our test scenario](#our-test-scenario)
- [Mocking an external module with vi.mock()](#mocking-an-external-module-with-vimock)
- [Mocking an environment variable with vi.stubEnv()](#mocking-an-environment-variable-with-vistubenv)
- [Restore mocked and stubbed objects](#restore-mocked-and-stubbed-objects)
- [Summary](#summary)

## Our test scenario

In this example, we have a backend function - `getPizzas` that:

- Fetches Pizza data from an external API,
- Filters the response according to a query received (using imported method `filterItems` from `/utils`),
- Returns the first 10 filtered results.  

```js
/** src/getPizzas.js */
import { filterItems } from './utils'

export const getPizzas = async (query) => {
  const API_URL = 'http://exploringvue.com/.netlify/functions'

  const response = await fetch(`${API_URL}/pizzas`)
    .then((res) => res.json())
    .catch((err) => {
      throw new Error("Failed to fetch pizzas");
    });

  const results = filterItems(response, "title", query);

  return {
    data: results.slice(0, 10),
    hasNextPage: results.length > 10,
  };
};
```

We want to test the above function in two levels:

- In isolation from the `filterItems` function, and the external call to the pizza API.
- Integration test without mocking the external call and with an a different API URL for testing purpose only.

Let's start with the first level, where we need to mock all the dependencies using `vi.mock()`, `vi.spyOn()` and `vi.fn()`.

## Mocking an external module with vi.mock()

`vi.mock()` allows us to mock an **external imported module** (_not an API or a global object_). It accepts two arguments: the module path and a factory function that returns the mocked module instance.

To mock our external module that contains the `filterItems` function, we can use `vi.mock()` as follows:

```js
/** getPizzas.test.js */
import { vi } from 'vitest';

vi.mock('./utils', async () => {
  return {
    filterItems: vi.fn().mockImplementation(
      () => [{ id: 1, title: 'Pizza 1' }]
     ),
  }
})

describe('getPizzas', () => {
  //...
});
```

In the above code, we passed the module path `./utils` and used `vi.fn()` to mock the `filterItems` function in the returned module instance. However, with this implementation, we are replacing the entire module with an instance that contains only the mocked `filterItems` method, which may not be the desired behavior in case `./utils` contains additional methods.

Fortunately, the factory function receives an asynchronous helper - `importOriginal` as its input, which gives us the original module, hence allowing us to keep the module structure, as shown below:

```js
vi.mock('./utils', async (importOriginal) => {
  const originalUtils = await importOriginal();
  return {
    ...originalUtils,
    filterItems: vi.fn().mockImplementation(
      () => [{ id: 1, title: 'Pizza 1' }]
     ),
  }
})
```

With this code, we have replaced `./utils` module with a new instance that contains both the original module methods and the mocked `filterItems` method.

**Note** that `vi.mock()` is hoisted to the top of the file, and Vitest will always execute it first, which means the following code will not work:

```js
const mockedFilteredItems = vi.fn().mockImplementation(() => []);

vi.mock('./utils', async (importOriginal) => {
  const originalUtils = await importOriginal();
  return {
    ...originalUtils,
    //ERROR: mockedFilteredItems is not defined
    filterItems: mockedFilteredItems 
  }
})
```

To avoid the above error, and enable the ability to mock the implementation of `filterItems` dynamically per test, we can perform the following as alternative:

- Call `vi.mock()` without a factory function
- Import the module using `import` and then use `vi.mocked()` to get the mocked method from the module.
- Change its implementation using `mockImplementation()` per test.

The below code demonstrates this approach:

```js  
//1. Import the module
import * as utils from './utils';

//2. Mock the module
vi.mock('./utils');

describe('getPizzas', () => {
  it('should return pizzas without next page', async () => {
    //3. Get the mocked method instance
    const mockedFilterItems = vi.mocked(utils.filterItems); 

    //4. Change the mocked implementation
    mockedFilterItems.mockImplementation(() => [{ id: 1, title: 'Pizza 1' }]);

    //5. Test the function
    //...
  });
});
```

Great. But we still can't run the test because we haven't mocked the external API call with `fetch` yet. Unfortunately, `vi.mock()` won't help us in mocking `global` module. Instead, we can mock `fetch` by using vi.fn() or vi.spyOn(), [from the previous blog post](https://dev.to/mayashavin/two-shades-of-mocking-a-function-in-vitest-41im).

Once we have mocked the `fetch` call, we can now complete the first test for the `getPizzas` function in isolation, as follows:

```js
import utils from './utils';

vi.mock('./utils');

describe('getPizzas', () => {
  const fetchSpy = vi.spyOn(global, 'fetch');
  
  it('should return pizzas without next page', async () => {
    const mockedFilterItems = vi.mocked(utils.filterItems); 
    const mockResponse = [{ id: 1, title: 'Pizza 1' }];
    mockedFilterItems.mockImplementation(() => mockResponse);

    fetchSpy.mockResolvedValue({
      ok: true,
      json: async () => mockResponse,
    });

    const result = await getPizzas();
    expect(result.data.length).toBe(1);
    expect(result.hasNextPage).toBe(false);
  });
});
```

As you can see in the above test, we have mocked both the `filterItems` function and the `fetch` call. In reality, this may not be the best approach. As `vi.mock()` receives the module path as its first argument, hence if the module path changes, our tests may break.

Also, `filterItems` is a simple function, and we could test it together with `getPizzas` without mocking. We should use `vi.mock()` only when we need to mock an external module that contains complex logic or side effects. Otherwise, it's recommended to use `vi.fn()` or `vi.spyOn()` to mock the function directly.

At this point, we explored writing unit test using `vi.mock()` for `getPizzas()`. Next, we will discuss how to write an integration test for the same function, where we will mock the external API path, which is an environment variable.

## Mocking an environment variable with vi.stubEnv()

Let's get back to our implementation of `getPizzas`, and replace the static API URL with an environment variable `API_URL`:

```js
/** getPizzas.js */
import { filterItems } from './utils'

export const getPizzas = async (query) => {
  const API_URL = process.env["API_URL"];

  //...
};
```

When we need to test the `getPizzas` function with a different API URL, we can use `vi.stubEnv()` to mock this environment variable as follows:

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


describe('getPizzas', () => {
  it('returns a list of pizzas', async () => {
    vi.stubEnv('API_URL', 'http://localhost:3000/testdata') //the target testing URL

    //Assertion as normal
    //...
  });
});
```

Assuming that `http://localhost:3000/testdata` is a testing URL that returns a list of pizzas, we have just completed the first integration test for `getPizzas`. This approach allows us to test the function with different API URLs without changing the actual environment variable's value.

Lastly, rule of thumbs: anything we mock/stub, we may need to restore it before/after each test. We will do it next.

## Restore mocked and stubbed objects

To restore the all the stubbed environment variables, we can trigger `vi.unstubAllEnvs()` after each test run, and in the below code:

```js
describe('getPizzas', () => {
  afterEach(() => {
    vi.unstubAllEnvs();
  });

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

Alternatively, we can achieve the same by setting `unstubEnvs` to `true` in the `vitest.config.js` file.

```js
/** vitest.config.js */
export default defineConfig({
  test: {
    //..
    unstubEnvs: true,
  },
})
```

This way, we can ensure that Vitest restores all environment variables before each test run.

For mocked module by `vi.mock()`, the best way is to set `mockReset: true` in the `vitest.config.js` file as follows:

```js
/** vitest.config.js */
export default defineConfig({
  test: {
    //..
    mockReset: true,
  },
})
```

## Summary

We have learnt how to mock an external module using `vi.mock()` and stub an environment variable using `vi.stubEnv()` in Vitest, as well as when it is best for. We also learned different ways to restore the mocked and stubbed objects. With these techniques, we can write efficient tests for our functions, ensuring that they are isolated and independent of external dependencies when needed.
  
👉 _Learn about Vue 3 in 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? [Buy me a coffee ☕](https://www.buymeacoffee.com/vtlrkh6) 🙌🏼