Vitestで誤解の多いStub, Spy, Mock, Fakeの違いについて

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(スタブ)→ 状態の再現

テスト対象のコードを実行させるために、「呼び出されたら決まった値を返す」だけの代役です。

返り値やエラーの送出をコントロールして、特定の条件(例外系や境界値)を再現する目的で使います。

sample.spec.ts
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(スパイ)→ 記録と傍観

元のオブジェクトやメソッドの実際の処理を維持したまま、呼び出し回数や引数を裏で記録します。

実装を破壊せずに「正しく呼ばれたか」を確認したい場合に利用します。

sample.spec.ts
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は「出力・呼び出しの検証」に重点が置かれます。

sample.spec.ts
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クラスを使ったり、タイマー処理を疑似実行したりするケースが該当します。

sample.spec.ts
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などでテストコードを書く場合は必ず書いてください。