vi.mock再入門

## はじめに

この記事は、[Vitest](https://vitest.dev/)における[モック](https://vitest.dev/guide/mocking.html)方法を整理するためのものです。

Vitestにはさまざまなモックの方法があり使うたびに混乱します。それぞれの関数にどのような目的があるのか整理のためにこの記事を作成しました。

### 想定する読者

- Vitestを使ったことがある
- JestなどのJavaScriptテストフレームワークを使ったことがある

:::note warn
この記事はすでにJavaScriptテストフレームワークを使ったことがある人を想定しています。ツールそのものの解説やインストールガイドなどは含まれません。
:::

### 想定する環境

- Vitest v2.1.8


:::note warn
この記事は、Vitest v2.1.8での挙動について記載しています。将来のバージョンでは挙動が変わるかもしれないので、記事を読む前にお手元の環境のバージョンを確認してください。
:::

## Vitestにおけるモック

Vitestにおけるモックには、おおまかに以下の方法があります。

| モック方法       | 使う場面の例                                     | 主な特徴                               |
|------------------|------------------------------------------------|----------------------------------------|
| `vi.fn()`        | 任意の関数をテスト対象に渡す場合                  | シンプルなモック                       |
| `vi.spyOn()`     | 既存のオブジェクトやグローバル関数を部分的にモック化する場合 | 元の実装をモックで置き換え可能         |
| `vi.mock()`      | モジュール全体をモック化し、テスト対象から切り離す場合 | 高度な依存性切り離し、巻き上げ注意が必要。リセットしてもオリジナル実装に戻らない |

それぞれの方法を使い分けることで、テストの目的に合わせたモックを作成できます。

### vi.fn()

```ts
import { vi } from "vitest"
const func = vi.fn(() => 0);
```

[vi.fn()](https://vitest.dev/api/vi.html#vi-fn)は[モック関数](https://vitest.dev/api/mock.html#mock-functions)を生成するユーティリティ関数です。モック関数は自分自身が呼び出された回数や引数を記録します。モック関数を直接実行したり、テスト対象に渡して内部でどのように呼び出されるかが検証できます。

```ts
import { vi, expect } from "vitest"
const func = vi.fn(() => 0);

test(func); // テストしたい関数に、モック関数を渡す

expect(func).toHaveBeenCalled() // モック関数が1回以上呼び出されないとテスト失敗
expect(func).toHaveBeenCalledWith(5) // 引数が5で呼び出されないとテスト失敗
```

#### モックの実装

モック関数には、テスト時に元となる関数とは異なる[モック実装](https://vitest.dev/api/mock.html#mockimplementation)を設定できます。

```ts
import { vi, expect } from "vitest"
const func = vi.fn(() => 0);
const mockedFunc = func.mockImplementation(()=>5); //元の実装の代わりに、5を返す関数を設定

test(func); // テストしたい関数に、モック関数を渡す
expect(func).toHaveReturnedWith(5) // 0を返すオリジナル実装ではなく、5を返すモック実装が呼び出される
```

モック実装を設定すると、テストのスコープを制限できます。たとえばオリジナル関数がディスクアクセスやネットワーク接続に依存していても、テスト中は処理をスキップできます。

#### モックの履歴

vi.fn()インスタンスは、以下の履歴を保存しています。

- モック関数が呼び出された回数
- モック関数に渡された引数
- モック関数が返した値

意図通りにモック関数が呼び出されているかを[`expect`](https://vitest.dev/api/expect)で評価します。

### vi.spyOn()

```ts
import { vi } from "vitest"

const cart = {
  getApples: () => 42,
}
const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => 1);
```

[vi.spyOn()](https://vitest.dev/api/vi.html#vi-spyon)は、オブジェクトのメソッドをモック関数に差し替えるユーティリティ関数です。上記の例では、`cart.getApples`関数をモック関数に差し替えています。

vi.spyOn()は、オブジェクトに内包される関数の振る舞いを監視するために使われます。

差し替えられた関数`spy`は、vi.fn()で生成されるモック関数と同じものです。モック関数か否かの判定は[vi.isMockFunction](https://vitest.dev/api/vi.html#vi-ismockfunction)関数を利用します。

```ts
import { vi, expect } from "vitest"

const cart = {
  getApples: () => 42,
}
const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => 1);

cart.getApples();
expect(spy).toHaveBeenCalled(); // モック関数が1回以上呼び出される
expect(spy).toHaveReturnedWith(1); // モック実装が呼び出されるため、1が返る
```

vi.spyOn関数は`window`や`console`などのグローバルオブジェクトのモックにも使えます。

#### クラスの関数をモックする

今まで紹介した方法は、オブジェクトそのものへのspyOnであり、クラスで言うとインスタンスに相当するもののモック方法でした。クラスの関数を包括的にモックしたい場合は、インスタンスではなく`<class>.prototype`をspyOnします。

_▼src/calculator.ts_

```ts
export class Calculator {
  add (a, b) {
    return a + b;
  }
}
```

_▼test.spec.ts_

```ts
import { Calculator } from './src/calculator.ts'

vi.spyOn(Calculator.prototype, "add").mockImplementation(
  (a, b) => a + b + 3,
);
const calc = new Calculator();
expect(instance.add(1, 2)).toBe(6); // モック実装側のa + b + 3が返る
```

プロトタイプを対象にspyOnすると、その後に生成されるインスタンスがすべてモック実装に差し変わります。

#### モジュールの関数をモックする

モジュールの関数をspyOnする場合は、ファイルごと[名前空間インポート](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%AE_import)し、その名前空間をspyOnに指定します。

_▼func.ts_
```ts
export function sub(a: number, b: number): number {
  return a - b;
}
```

_▼test.spec.ts_
```ts
import * as Functions from "./func.ts"; // ユーティリティ関数を、名前空間としてimportする
import { sub } from "./func.ts"; // ユーティリティ関数もimportする

vi.spyOn(Functons, "sub").mockImplementation(
  (a, b) => a + b + 3,
); // 名前空間ごとspyOnする
expect(sub(1, 2)).toBe(6); // モック実装側のa + b + 3が返る
```

[公式ドキュメント](https://vitest.dev/guide/mocking.html#mock-an-exported-function)も合わせて参照してください。

### vi.mock()

```ts
import { Calculator } from './src/calculator.ts'

vi.mock('./src/calculator.ts');
```

[vi.mock()](https://vitest.dev/api/vi.html#vi-mock)はインポートパスをフックし、そのパスで呼び出されるモジュールすべてをモックします。上記の例では`Calculator`モジュールをモックしています。

:::note warn
vi.mock関数はファイルの先頭に巻き上げられて実行されます。vi.mockはテスト中や、beforeEachなどのフックの実行順を無視して先頭に記述したように振る舞います。コードの可読性のために、vi.mockはファイル先頭に記述することが推奨されています。

巻き上げをしないユーティリティ関数として[vi.doMock()](https://vitest.dev/api/vi.html#vi-domock)があります。
:::

モック関数を個別に取り出すには[mocked](https://vitest.dev/api/vi.html#vi-mocked)関数を使います。

_▼src/calculator.ts_

```ts
export class Calculator {
  add (a, b) {
    return a + b;
  }
}
```

_▼test.spec.ts_

```ts
import { Calculator } from './src/calculator.ts'

vi.mock('./src/calculator.ts');

const calc = new Calculator();
const spyAdd = vi.mocked(calc.add); //ここでaddのモック関数を取得する
```



## モック関数のクリア / リセット / レストア

モック関数には、自分自身の状態を書き換える関数が用意されています。

| 状態更新方法       | 使う場面の例                                     |
|------------------|------------------------------------------------|
| `mockClear()`        | モック関数の履歴（呼び出された回数や引数など）をクリアする                  | シンプルなモック                       |
| `mockReset()`     | 履歴をクリアし、モック実装を削除する |
| `mockRestore()`      | 履歴をクリアし、モック実装をオリジナル関数に戻す|

### clear

[mockClear](https://vitest.dev/api/mock.html#mockclear)は、以下のように動作します。

- モック関数の履歴をクリアする
- モック実装は維持する

```ts
  test("モッククリアは実装を維持する", () => {
    const func = vi.fn(() => 1);
    const mockedFunc = func.mockImplementation(() => 2); //モック関数は2を返す
    const result = mockedFunc();
    expect(mockedFunc).toBeCalledTimes(1); //呼び出された回数は1回
    expect(result).toBe(2); //モック関数が呼び出されたので、戻値は2

    func.mockClear(); //モックをクリア
    expect(mockedFunc).toBeCalledTimes(0); //呼び出し回数がリセットされる

    const result2 = func(); //2回目の呼び出し
    expect(mockedFunc).toBeCalledTimes(1); // 呼び出し回数が1回に戻る
    expect(result2).toBe(2); // モック関数の実装は維持される
  });
```

### reset

[mockReset](https://vitest.dev/api/mock.html#mockreset)は、以下のように動作します。

- モック関数の履歴をクリアする
- モック実装を破棄し、undefinedに置き換える

```ts
  test("モックリセットはモック実装を破棄する", () => {
    const func = vi.fn(() => 1);
    const mockedFunc = func.mockImplementation(() => 2);
    mockedFunc.mockReset();
    const result = mockedFunc();
    expect(result).toBe(undefined); // モック実装が破棄されたため、undefinedが返る
  });
```

### restore

[mockRestore](https://vitest.dev/api/mock.html#mockrestore)は、以下のように動作します。

- モック関数の履歴をクリアする
- モック実装を、置き換える前のオリジナルの関数に戻す

```ts
  test("モックレストアはオリジナルの実装に戻す", () => {
    const func = vi.fn(() => 1); // オリジナルの関数は1を返す
    const mockedFunc = func.mockImplementation(() => 2); // モック実装は2を返す
    const result = mockedFunc();
    expect(mockedFunc).toBeCalledTimes(1);
    expect(result).toBe(2); // モック実装が呼び出されるため、2が返る

    mockedFunc.mockRestore(); // モックをレストアする
    expect(mockedFunc).toBeCalledTimes(0); // モック関数の履歴がリセットされる
    const result2 = mockedFunc();
    expect(result2).toBe(1); // モック関数の実装がオリジナルに戻るため、1が返る
  });
```

### vi.clear / reset / restoreAllMocks

- [vi.clearAllMocks](https://vitest.dev/api/vi.html#vi-clearallmocks)
- [vi.resetAllMocks](https://vitest.dev/api/vi.html#vi-resetallmocks)
- [vi.restoreAllMocks](https://vitest.dev/api/vi.html#vi-restoreallmocks)

vi.**AllMocksは、mockClear, mockReset, mockRestoreをすべてのモック関数に適用するユーティリティ関数です。spyOnやmockも内部でモック関数を利用しているため、このユーティリティ関数で再設定できます。

## vi.spyOnとvi.mockの違いと使い分け

vi.spyOnとvi.mockの違いを確認し、それぞれの使い分けを検討しましょう。

### モックの対象

vi.spyOnとvi.mockの相違点のひとつは、モックの対象です。

- vi.spyOn : オブジェクトが対象
- vi.mock : importパスが対象

vi.mockの対象はimportパスです。したがって[外部ファイルしかモックできません](https://vitest.dev/guide/mocking#mocking-pitfalls)。

### モックのタイミング

vi.spyOnとvi.mockのもうひとつの相違点は、モック関数が生成されるタイミングです。

- vi.spyOn : モジュールが読み込まれた**後**に、関数をモックする
- vi.mock : モジュールが読み込まれる**前**に割り込む

vi.mockはデフォルトではオリジナルの実装を呼び出さず、すべて`undefined`を返します。

:::note warn
### vi.mock()したモジュールは、restoreされない

vi.mock()でのモックは、モジュールの読み込み前に実行されます。そのため、vi.mockの対象になったモジュールの関数にはオリジナル実装がありません。

vi.mocked()で取り出したモック関数にリセットをかけると、モック実装はオリジナルには戻らす`undefined`になります。
:::

:::note

vi.mockを使ってモジュールのオリジナル実装を呼び出し、一部の実装だけをモックする方法は[公式ドキュメント](https://vitest.dev/guide/mocking.html#mock-part-of-a-module)に記載されています。

:::

### 使い分け

誤ってテスト中に呼び出してはいけないモジュールをテストから切り離す場合、vi.mockの採用を検討してください。

たとえば

 - ネットワークアクセスやディスクアクセスなど、外部環境に依存するモジュールをテストから切り離したい場合は`vi.mock`を使う
 - 一部の関数の呼び出しを監視するためだけにモックする場合は`vi.spyOn`を使う

のように、テストからモジュールを切り離す度合いに応じてこの2つを使い分けてください。

https://github.com/vitest-dev/vitest/discussions/4224

vi.spyOnとvi.mockの使い分けについては、GitHub上で議論が行われています。ぜひご参照ください。


## 個人的な感想

Vitestでモック関数を利用する方法は複数ありますが、すべてvi.fn()を使いやすくするためのユーティリティであると考えれば、お互いの関係が理解しやすくなります。vi.**AllMocksも、モック関数の状態操作を呼び出しています。

クリア、リセット、レストアの状態操作処理はテスト結果に影響します。どのような目的で利用されるかを整理すれば状態操作で悩む時間が減ります。

以上、ありがとうございました。