🦮

Vitestのモックについて

に公開

Vitest

vitestのモックについてまとめました.

Vitestのvi.fn()がなにをしているのか

const func = vi.fn(() => 5)
console.log(func)
func();
expect(func).toHaveBeenCalled()
expect(func).toHaveReturnedWith(5)

この場合、vi.fn()は戻り値として5を返すモック関数を生成し、その関数をfuncに代入しています。
vi.fn()を使用すると、引数に指定した関数のモックを作成し、呼び出し回数や引数の記録、戻り値の変更などが可能になります。

console.log(func)の結果は以下のようになります。
func.mockReturnValueを使えば、戻り値を動的に変更することも可能です。
詳細は公式ドキュメントをご覧ください:
Vitest Mock Functions

[Function: spy] {
  getMockName: [Function (anonymous)],
  mockName: [Function (anonymous)],
  mockClear: [Function (anonymous)],
  mockReset: [Function (anonymous)],
  mockRestore: [Function (anonymous)],
  getMockImplementation: [Function (anonymous)],
  mockImplementation: [Function (anonymous)],
  mockImplementationOnce: [Function (anonymous)],
  withImplementation: [Function: withImplementation],
  mockReturnThis: [Function (anonymous)],
  mockReturnValue: [Function (anonymous)],
  mockReturnValueOnce: [Function (anonymous)],
  mockResolvedValue: [Function (anonymous)],
  mockResolvedValueOnce: [Function (anonymous)],
  mockRejectedValue: [Function (anonymous)],
  mockRejectedValueOnce: [Function (anonymous)]
}

vi.mock()について

vi.mock()を使用することで指定されたパスの関数をモックすることができます.
第一引数にモックしたい関数のパスを指定します.
第二引数にモックしたい関数とモックが返す関数を設定することができます.
また, vi.mock()はホイスティングされてしまうのでグローバル変数にアクセスすることができません.

使用例

// sum.ts
export const sum = (a: number, b: number) => a + b

// vitest.test.ts
import '@testing-library/jest-dom/vitest'
import { test, expect, vi } from 'vitest'
import { sum } from './sum'

vi.mock('./sum', () => ({
  sum: () => 100,
}))

test('vi.mock', () => {
  expect(sum(1, 2)).toBe(100)
})

エラーになるケース1

下記のコードの場合, vi.mock()の中でグローバル変数であるreturnValueにアクセスしているため, エラーが発生します.また, モックが返す値には変数ではなく, 関数を設定する必要があります.

// sum.ts
export const sum = (a: number, b: number) => a + b

// vitest.test.ts
const returnValue = 100
vi.mock('./sum', () => ({
  sum: returnValue,
}))

test('vi.mock', () => {
  expect(sum(1, 2)).toBe(100)
})

FAIL  src/pages/blogs/vitest.test.tsx [ src/pages/blogs/vitest.test.tsx ]
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
 ❯ src/pages/blogs/vitest.test.tsx:6:25
      4| 
      5| const returnValue = 3
      6| vi.mock('./sum', () => ({
       |                         ^
      7|   sum: returnValue,
      8| }))

Caused by: ReferenceError: Cannot access 'returnValue' before initialization

解決策

下記のように, 関数を返すようにすると問題なく動作します.

vi.mock('./sum', () => ({
  sum: () => returnValue,
}))

エラーになるケース2

この場合もやはり, vi.mock()がホイスティングされるので, グローバル変数にはアクセスすることができません.
mockReturnValue(returnValue)を使い,返す値をモックしているが, このときにグローバル変数であるreturnValueにアクセスしているため, エラーが発生します.

// sum.ts
export const sum = (a: number, b: number) => a + b

// vitest.test.ts
const returnValue = 3
vi.mock('./sum', () => ({
  sum: vi.fn().mockReturnValue(returnValue),
}))

test('vi.mock', () => {
  expect(sum(1, 2)).toBe(3)
})

FAIL  src/pages/blogs/vitest.test.tsx [ src/pages/blogs/vitest.test.tsx ]
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
 ❯ src/pages/blogs/vitest.test.tsx:6:25
      4| 
      5| // vi.mock('./sum', () => ({
      6| //   sum: () => 3,
       |                         ^
      7| // }))
      8| 

Caused by: ReferenceError: Cannot access 'returnValue' before initialization

解決策

下記のようなコードにすることで, sumが呼び出されるまで, returnValueにアクセスすることを遅らせることができるため, 正しく動作します.

vi.mock('./sum', () => ({
  sum: vi.fn(() => returnValue),
}))

テスト内でvi.mock()でモックされた関数が返す値を変更するには

テスト内でモック関数の戻り値を変更するには、以下のようにします。
この場合, vi.mock()が実行されたときには, グローバル変数であるmockSumにはアクセスできません.
しかし, sumが呼び出されるのはテスト内であるため, テスト内でmockSumの動作を設定すれば問題なく動作します.

// sum.ts
export const sum = (a: number, b: number) => a + b

// vitest.test.ts
let mockSum: (a: number, b: number) => number
vi.mock('./sum', () => ({
  sum: (a: number, b: number) => mockSum(a, b),
}))

test('vi.mock', () => {
  mockSum = vi.fn(() => 100) //  sumが返す値を100に変更
  sum(1, 2) // sumを呼び出す
  expect(mockSum).toBeCalled() // mockSumが呼び出されたか確認
  expect(sum(1, 2)).toBe(100) // sumが100を返すか確認
})

Discussion