iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💯

Changing vi.mock Return Values for Each Test Case in Vitest

に公開

As the title suggests, I want to change the return value of a function mocked with vi.mock for each test case.

I eventually got it working, but I couldn't find exact information by searching on Google or asking AI. There might be a better way, but for now, this is a memo on how I managed to do it.

Prerequisites

Suppose you have files organized like this:

src
├── my-module.ts
└── sample.ts
my-module.ts
export const getMyValue = async () => {
  return 'myValue';
};
sample.ts
import { getMyValue } from './my-module';

export const getMyValueWrapper = async () => {
  const myValue = await getMyValue();
  return myValue;
};

This time, I want to mock getMyValue() from my-module.ts using vi.mock while writing tests for sample.ts.

First, the complete solution

To get straight to the point, I managed to do it by combining vi.fn and vi.hoisted.

sample.test.ts
import { test } from 'vitest';

import { getMyValueWrapper } from './sample';

const mockGetMyValue = vi.hoisted(() => {
  return vi.fn();
});

vi.mock('./my-module', () => {
  return {
    getMyValue: mockGetMyValue,
  };
});

beforeEach(() => {
  mockGetMyValue.mockClear();
});

test('foo', async () => {
  mockGetMyValue.mockResolvedValue('foo');
  expect(await getMyValueWrapper()).toBe('foo');
});

test('bar', async () => {
  mockGetMyValue.mockResolvedValue('bar');
  expect(await getMyValueWrapper()).toBe('bar');
});

test('baz', async () => {
  mockGetMyValue.mockResolvedValue('baz');
  expect(await getMyValueWrapper()).toBe('baz');
});

By writing it this way, I was able to change the return value of the mocked function for each test case.

If you want to set a default return value

If you want to set a default return value, you can write mockGetMyValue.mockResolvedValue() in beforeEach.

sample.test.ts
beforeEach(() => {
  mockGetMyValue.mockClear();

  // デフォルト設定
  mockGetMyValue.mockResolvedValue('default');
});

test('default', async () => {
  // テストケース内で値を設定しなくても `'default'` になる
  expect(await getMyValueWrapper()).toBe('default');
});

About vi.mock, vi.hoisted, and hoisting

vi.mock is hoisted to the top of the file during execution.

So, if you write it as follows, it will not work correctly:

import { test } from 'vitest';

import { getMyValueWrapper } from './sample';

test('foo', async () => {
  vi.mock('./my-module', () => {
    return {
      getMyValue: async () => 'foo',
    };
  });
  expect(await getMyValueWrapper()).toBe('foo'); // `actual: 'baz'` になって失敗
});

test('bar', async () => {
  vi.mock('./my-module', () => {
    return {
      getMyValue: async () => 'bar',
    };
  });
  expect(await getMyValueWrapper()).toBe('bar'); // `actual: 'baz'` になって失敗
});

test('baz', async () => {
  vi.mock('./my-module', () => {
    return {
      getMyValue: async () => 'baz',
    };
  });
  expect(await getMyValueWrapper()).toBe('baz'); // これだけ成功する
});

All but the last test will fail. Why is that?

After transpilation, the test file is likely to look like this:

Transpiled (image)
// vi.mockはファイルの先頭に巻き上げられる
vi.mock('./my-module', () => {
  return {
    getMyValue: async () => 'foo',
  };
});
vi.mock('./my-module', () => {
  return {
    getMyValue: async () => 'bar',
  };
});
vi.mock('./my-module', () => {
  return {
    getMyValue: async () => 'baz',
  };
});

import { test } from 'vitest';

import { getMyValueWrapper } from './sample';

test('foo', async () => {
  expect(await getMyValueWrapper()).toBe('foo');
});

test('bar', async () => {
  expect(await getMyValueWrapper()).toBe('bar');
});

test('baz', async () => {
  expect(await getMyValueWrapper()).toBe('baz');
});

Because of this, only the last

vi.mock('./my-module', () => {
  return {
    getMyValue: async () => 'baz',
  };
});

becomes effective, and the return value for all tests becomes 'baz'.

Therefore, to prevent misunderstandings during code reviews by team members, it's generally best to write vi.mock near the top of the file and at the top level.

Also, by using vi.hoisted, you can define variables by hoisting them to the top of the file.

sample.test.ts
const mockGetMyValue = vi.hoisted(() => {
  return vi.fn();
});

vi.mock('./my-module', () => {
  return {
    getMyValue: mockGetMyValue,
  };
});

If you do this, it will be transpiled as follows (this is just an image of how it would be):

Transpiled (image)
// vi.mockより先頭に巻き上げて変数を定義できる
const mockGetMyValue = (() => vi.fn())();

vi.mock('./my-module', () => {
  return {
    getMyValue: mockGetMyValue,
  };
});

Once you reach this point, you can set the return value for each test case using .mockResolvedValue() (or .mockReturnValue() if it's not an async function).

Other examples of code that doesn't work

When setting default values, I initially wrote the following code and got stuck for a moment because it didn't work.

sample.test.ts
const mockGetMyValue = vi.hoisted(() => {
  return vi.fn();
});

vi.mock('./my-module', () => {
  return {
    // Set default return value?
    getMyValue: mockGetMyValue.mockResolvedValue('default'),
  };
});

beforeEach(() => {
  mockGetMyValue.mockClear();
});

test('default', async () => {
  expect(await getMyValueWrapper()).toBe('default'); // Fails with `actual: 'undefined'`
});

Intuitively, I thought it would work, but the function's return value ended up being undefined.

console.log(mockGetMyValue.constructor.name); // => `Function`
console.log(mockGetMyValue.mockResolvedValue('default').constructor.name); // => `Function`

Since .mockResolvedValue() also returns a Function instance, I thought it might work 🤔 In any case, you need to use the original mock instance (I haven't delved deeper into this).

No one else might fall into the same trap, but I introduced it just in case.

That's all. The transpiled code was written based on my imagination from the documentation, so please point out any misunderstandings.

References

First, the official documentation.

https://vitest.dev/api/vi.html

https://vitest.dev/guide/mocking

And this article was a great help in understanding. Many thanks.

https://zenn.dev/you_5805/articles/vitest-mock-hoisting

Discussion