はじめに
Vitestでテストを書いていてモックの使い方に悩むことが多かったので、整理の意味も込めて Vitestにおけるモックの基礎をまとめてみました。
テストの基本的な考え方(describe
や it
、フックなど)については、以前まとめたこちらの記事をご覧ください。
モックとは?
テストを書くときに、本物の関数や外部サービス(APIやDB、外部ライブラリなど)を利用すると困ることがあります。
- 実行に時間がかかる
- ネットワークやDBが必要で不安定になる
- 本物の処理を呼ぶと副作用(データ更新など)が起きてしまう
こうした「テストの邪魔になる依存」を切り離して、代わりに用意するダミーの部品を「モック」と言い、モックを使うことで、テストを以下のようにシンプルにできます。
- 早い(余計な処理をスキップできる)
- 安全(本番のDBやAPIに影響しない)
- コントロールしやすい(返す値や動きを自由に決められる)
スタブ・スパイとの違い
モックと似た言葉にスタブやスパイがあります。
厳密な区別はありますが、ざっくり以下のような役割を持ちます。
- スタブ:「呼ばれたら決まった値を返す」
- スパイ:「呼ばれた回数や引数を記録する」
- モック:「スタブのように値を返すこともでき、スパイのように呼び出しを確認することもできる」
普段の会話では上記をすべてまとめて「モック」と呼ぶことが多く、Vitest では vi.fn()
や vi.spyOn()
を使うことで、こうした動きを簡単に再現できます。
Vitestでのモックの基本
vi.fn()
新しく「モック関数」を作るための仕組みです。
呼び出し履歴を記録したり、返り値を自由に設定したりできる、一番基本的なモックの使い方です。
使い方の例
const mockFn = vi.fn() // モック関数を作る
mockFn("hello") // 呼び出し
expect(mockFn).toHaveBeenCalled() // 呼ばれたことを確認
expect(mockFn).toHaveBeenCalledWith("hello") // 引数を確認
返り値を設定することもできます👇
const mockFn = vi.fn().mockReturnValue("fixed value")
expect(mockFn()).toBe("fixed value")
制限・注意点
- 元の関数を置き換えるわけではない
→vi.fn()
はまっさらな関数を作るだけなので、既存の関数を差し替えたい場合はvi.spyOn
を使う - 呼ばれ方の記録はテスト間で共有されない
→ 各テストごとにモック関数を作り直すのが基本
実際に使う場面
「自分でダミー関数を用意して、呼び出しを記録・検証したい」ときに使います。
- 外部から受け取る関数をテスト用に置き換える
function runTask(callback: (msg: string) => void) { callback("task done") } const mockFn = vi.fn() runTask(mockFn) expect(mockFn).toHaveBeenCalledWith("task done")
- 関数の呼び出し回数や引数を検証する
- API 呼び出し関数を渡したときに、正しい引数で呼ばれているかをチェック
- ボタン押下時にイベントハンドラが呼ばれているかをチェック
vi.spyOn()
既存のオブジェクトや関数を「監視」したり「差し替え」したりするために使います。
vi.fn()
は自分で新しくモック関数を作りますが、vi.spyOn()
は既存の関数をモック化する点が違います。
使い方の例
vi.spyOn(対象オブジェクト, "監視したい関数名")
という書き方で利用します。
const user = {
getName: () => "Alice",
};
// getName を spyOn で監視対象にする
const spy = vi.spyOn(user, "getName")
// 返り値を「Bob」に差し替える
spy.mockReturnValue("Bob")
expect(user.getName()).toBe("Bob"); // 差し替えた返り値を確認
expect(spy).toHaveBeenCalled(); // モック関数が呼ばれたことを確認
// 元に戻す
spy.mockRestore()
制限・注意点
- 監視できるのはオブジェクトのメソッドだけ
- 例:
console.log
,Date.now
- グローバル関数やトップレベルの関数は直接
spyOn
できない
- 例:
- 他のテストに影響することがあるためテスト後は元に戻す
実際に使う場面
「既存の関数の動きを一時的に差し替えたいとき」に便利です。
- ログ出力が呼ばれたか確認する
const spy = vi.spyOn(console, "log") console.log("Hello") expect(spy).toHaveBeenCalledWith("Hello") spy.mockRestore()
- 現在時刻を固定する
const spy = vi.spyOn(Date, "now").mockReturnValue(123456) expect(Date.now()).toBe(123456) spy.mockRestore()
vi.mock()
モジュールをまるごと差し替えるときに使います。
使い方の例
- 自動モック:モジュールがエクスポートしている関数をすべて
vi.fn()
に置き換えるvi.mock("./api")
- 手動モック:どうモックするか自分で定義
vi.mock("./api", () => { return { fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: "Alice" })) } })
制限・ルール
- モックはファイルの先頭でしか使えない
- Vitest がテストを読み込むときに、自動的にファイルの先頭に持ち上げて処理するので、
test
やbeforeEach
の中に書いても、その場では動かない
- Vitest がテストを読み込むときに、自動的にファイルの先頭に持ち上げて処理するので、
- 条件分岐の中では使えない
if (process.env.NODE_ENV === "test") { vi.mock("./api") // ❌ こういう条件分岐は効かない }
- テストごとに切り替えられない
- 「テストAではこのモック、テストBでは別のモック」といった切り替えは基本できない
→ どうしても切り替えたい場合はvi.doMock
を使う方法がある
- 「テストAではこのモック、テストBでは別のモック」といった切り替えは基本できない
- モックはファイルごとに有効
- 一度モックすると、そのテストファイルの中ではずっとそのモックが使われる
- 別のテストファイルには影響しない
実際に使う場面
- 外のサービスにアクセスする処理を置き換える
- 例: ユーザー情報をサーバーから取得する関数(API呼び出し)
- 本物のデータベースにつなぐ処理を止める
- 例: 実際のDBに書き込みたくないので、代わりのモックを使う
- 本物の認証サービスを呼ばないようにする
- 例: ログインチェックやトークン取得をテスト用の関数に差し替える
モック利用の考え方
書きたいテストによって使い分けることが大前提ですが、モックは小さい単位から順に使うように考えるのが基本です。
-
vi.fn
: 新しいモック関数を作る(まずはこれで十分なことが多い) -
vi.spyOn
: 既存の関数を監視・差し替えたいとき -
vi.mock
: モジュール全体を丸ごとモックしたいとき(最後の手段)
参考