はじめに
この記事は、Vitestにおけるモック方法を整理するためのものです。
Vitestにはさまざまなモックの方法があり使うたびに混乱します。それぞれの関数にどのような目的があるのか整理のためにこの記事を作成しました。
想定する読者
- Vitestを使ったことがある
- JestなどのJavaScriptテストフレームワークを使ったことがある
この記事はすでにJavaScriptテストフレームワークを使ったことがある人を想定しています。ツールそのものの解説やインストールガイドなどは含まれません。
想定する環境
- Vitest v2.1.8
この記事は、Vitest v2.1.8での挙動について記載しています。将来のバージョンでは挙動が変わるかもしれないので、記事を読む前にお手元の環境のバージョンを確認してください。
Vitestにおけるモック
Vitestにおけるモックには、おおまかに以下の方法があります。
モック方法 | 使う場面の例 | 主な特徴 |
---|---|---|
vi.fn() |
任意の関数をテスト対象に渡す場合 | シンプルなモック |
vi.spyOn() |
既存のオブジェクトやグローバル関数を部分的にモック化する場合 | 元の実装をモックで置き換え可能 |
vi.mock() |
モジュール全体をモック化し、テスト対象から切り離す場合 | 高度な依存性切り離し、巻き上げ注意が必要。リセットしてもオリジナル実装に戻らない |
それぞれの方法を使い分けることで、テストの目的に合わせたモックを作成できます。
vi.fn()
import { vi } from "vitest"
const func = vi.fn(() => 0);
vi.fn()はモック関数を生成するユーティリティ関数です。モック関数は自分自身が呼び出された回数や引数を記録します。モック関数を直接実行したり、テスト対象に渡して内部でどのように呼び出されるかが検証できます。
import { vi, expect } from "vitest"
const func = vi.fn(() => 0);
test(func); // テストしたい関数に、モック関数を渡す
expect(func).toHaveBeenCalled() // モック関数が1回以上呼び出されないとテスト失敗
expect(func).toHaveBeenCalledWith(5) // 引数が5で呼び出されないとテスト失敗
モックの実装
モック関数には、テスト時に元となる関数とは異なるモック実装を設定できます。
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
で評価します。
vi.spyOn()
import { vi } from "vitest"
const cart = {
getApples: () => 42,
}
const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => 1);
vi.spyOn()は、オブジェクトのメソッドをモック関数に差し替えるユーティリティ関数です。上記の例では、cart.getApples
関数をモック関数に差し替えています。
vi.spyOn()は、オブジェクトに内包される関数の振る舞いを監視するために使われます。
差し替えられた関数spy
は、vi.fn()で生成されるモック関数と同じものです。モック関数か否かの判定はvi.isMockFunction関数を利用します。
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
export class Calculator {
add (a, b) {
return a + b;
}
}
▼test.spec.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する場合は、ファイルごと名前空間インポートし、その名前空間をspyOnに指定します。
▼func.ts
export function sub(a: number, b: number): number {
return a - b;
}
▼test.spec.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が返る
vi.mock()
import { Calculator } from './src/calculator.ts'
vi.mock('./src/calculator.ts');
vi.mock()はインポートパスをフックし、そのパスで呼び出されるモジュールすべてをモックします。上記の例ではCalculator
モジュールをモックしています。
vi.mock関数はファイルの先頭に巻き上げられて実行されます。vi.mockはテスト中や、beforeEachなどのフックの実行順を無視して先頭に記述したように振る舞います。コードの可読性のために、vi.mockはファイル先頭に記述することが推奨されています。
巻き上げをしないユーティリティ関数としてvi.doMock()があります。
モック関数を個別に取り出すにはmocked関数を使います。
▼src/calculator.ts
export class Calculator {
add (a, b) {
return a + b;
}
}
▼test.spec.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は、以下のように動作します。
- モック関数の履歴をクリアする
- モック実装は維持する
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は、以下のように動作します。
- モック関数の履歴をクリアする
- モック実装を破棄し、undefinedに置き換える
test("モックリセットはモック実装を破棄する", () => {
const func = vi.fn(() => 1);
const mockedFunc = func.mockImplementation(() => 2);
mockedFunc.mockReset();
const result = mockedFunc();
expect(result).toBe(undefined); // モック実装が破棄されたため、undefinedが返る
});
restore
mockRestoreは、以下のように動作します。
- モック関数の履歴をクリアする
- モック実装を、置き換える前のオリジナルの関数に戻す
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.**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パスです。したがって外部ファイルしかモックできません。
モックのタイミング
vi.spyOnとvi.mockのもうひとつの相違点は、モック関数が生成されるタイミングです。
- vi.spyOn : モジュールが読み込まれた後に、関数をモックする
- vi.mock : モジュールが読み込まれる前に割り込む
vi.mockはデフォルトではオリジナルの実装を呼び出さず、すべてundefined
を返します。
vi.mockを使ってモジュールのオリジナル実装を呼び出し、一部の実装だけをモックする方法は公式ドキュメントに記載されています。
使い分け
誤ってテスト中に呼び出してはいけないモジュールをテストから切り離す場合、vi.mockの採用を検討してください。
たとえば
- ネットワークアクセスやディスクアクセスなど、外部環境に依存するモジュールをテストから切り離したい場合は
vi.mock
を使う - 一部の関数の呼び出しを監視するためだけにモックする場合は
vi.spyOn
を使う
のように、テストからモジュールを切り離す度合いに応じてこの2つを使い分けてください。
vi.spyOnとvi.mockの使い分けについては、GitHub上で議論が行われています。ぜひご参照ください。
個人的な感想
Vitestでモック関数を利用する方法は複数ありますが、すべてvi.fn()を使いやすくするためのユーティリティであると考えれば、お互いの関係が理解しやすくなります。vi.**AllMocksも、モック関数の状態操作を呼び出しています。
クリア、リセット、レストアの状態操作処理はテスト結果に影響します。どのような目的で利用されるかを整理すれば状態操作で悩む時間が減ります。
以上、ありがとうございました。