
4つのテストダブルの基本概念について
Vitestを使ってテストを書いていると、「関数をダミーに差し替える」ための機能が多くて混乱することがあります。
特にStub、Spy、Mock、Fakeの4つは混同されがちです。
それぞれの本来の役割と、Vitestでの具体的な実現方法を整理してまとめました。
4つの基本概念と違い
テストダブル(テスト用の身代わりオブジェクト)の分類において、各概念は以下のように定義されます。
| 分類 | 目的 | Vitestでの主な実現方法 |
|---|---|---|
| Stub | 決まった値を返し、テスト対象に必要な状態を作る | vi.fn().mockReturnValue() / vi.stubGlobal() |
| Spy | 本物の処理を実行しつつ、呼び出し履歴を記録する | vi.spyOn() |
| Mock | 呼び出し方や振る舞いが正しいかを検証する | vi.fn() + expect().toHaveBeenCalled() |
| Fake | 簡易的な動く実装を提供して本物の代わりにする | vi.useFakeTimers() など |
Stub(スタブ)→ 状態の再現
テスト対象のコードを実行させるために、「呼び出されたら決まった値を返す」だけの代役です。
返り値やエラーの送出をコントロールして、特定の条件(例外系や境界値)を再現する目的で使います。
import { expect, test, vi } from 'vitest'
// APIクライアントの挙動をスタブ化(常に決まったレスポンスを返す)
const fetchUserStub = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
test('ユーザー名が表示されること', async () => {
const user = await fetchUserStub()
expect(user.name).toBe('Alice')
})
// グローバルオブジェクトのスタブ化
vi.stubGlobal('location', { href: 'https://example.com' })Spy(スパイ)→ 記録と傍観
元のオブジェクトやメソッドの実際の処理を維持したまま、呼び出し回数や引数を裏で記録します。
実装を破壊せずに「正しく呼ばれたか」を確認したい場合に利用します。
import { expect, test, vi } from 'vitest'
const calculator = {
add: (a: number, b: number) => a + b
}
test('addメソッドの呼び出しをスパイする', () => {
// 元の処理を保持したままスパイを仕込む
const spy = vi.spyOn(calculator, 'add')
const result = calculator.add(2, 3)
// 本物の処理が実行されている
expect(result).toBe(5)
// 呼び出し履歴も記録されている
expect(spy).toHaveBeenCalledWith(2, 3)
spy.mockRestore()
})Mock(モック)→ 振る舞いの検証
単に値を返すだけでなく、「どのように呼び出されたか(呼び出し回数、引数の順序など)」の振る舞いそのものを検証するために使用します。
Stubが「入力データの準備」であるのに対し、Mockは「出力・呼び出しの検証」に重点が置かれます。
import { expect, test, vi } from 'vitest'
test('通知関数が正しいメッセージで呼ばれること', () => {
const sendNotificationMock = vi.fn()
// テスト対象の処理を実行
sendNotificationMock('Hello, Vitest!')
// 振る舞いの検証
expect(sendNotificationMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMock).toHaveBeenCalledWith('Hello, Vitest!')
})Fake(フェイク)→ 簡易的な実体
本物と同じように動作するものの、本番環境で使うには単純化されすぎている代替品です。
例えば、実際のDBの代わりにメモリ上で動くMapクラスを使ったり、タイマー処理を疑似実行したりするケースが該当します。
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
beforeEach(() => {
// タイマーの挙動をフェイクに差し替える
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('3秒後のタイマー処理が実行されること', () => {
const callback = vi.fn()
setTimeout(callback, 3000)
// 時間を疑似的に進める
vi.advanceTimersByTime(3000)
// リアルタイムで3秒待たずに実行できる
expect(callback).toHaveBeenCalled()
})まとめ → 混乱しやすいポイントの整理
- Stub vs Mock: 返り値を作ってテスト対象を動かすのが Stub。呼び出され方を検証するのが Mock。(Vitestではどちらも vi.fn() で作れるため混同しやすい)
- Spy vs Mock/Stub: 元のロジックを残すのが Spy。元のロジックを置き換えてしまうのが Mock / Stub。
- Fake vs 他の3つ: 関数のモック化ではなく、「軽量な代替ロジックを丸ごと用意する」のが Fake。
目的(状態のセットアップなのか、呼び出しの検証なのか、時間・外部環境の再現なのか)を意識して使い分けることで、意図が明確でメンテナンスしやすいテストコードが書けるようになります。
特にFakeは知らないと、new Date()で現在の日時を取得している処理の場合、常に同じテスト結果を取得できなくなってしまうので、Vitestなどでテストコードを書く場合は必ず書いてください。


