テストダブル(モック・スタブ)

テストダブルとは

テストダブル(Test Double)は、テスト対象が依存している本物の部品を、テストのために偽物へ差し替えたものの総称です。「ダブル」は映画のスタントダブル(代役)から来ています。

[[自動テスト]] の土台になるユニットテストは「関数・クラス単体」を検証するものですが、現実のコードは単体で完結していません。決済APIを叩き、データベースに書き、new Date() で現在時刻を読みます。この依存を代役に置き換えて初めて「単体」でテストできる — その代役がテストダブルです。[[テスト駆動開発]] の「呼び出しにくいコードはテストも書きにくい」に最初にぶつかるのがここです。

なぜ本物を使わないのか

依存本物を使うと起きること
外部API遅い(1回200ms × 100テストで20秒)。相手が落ちるとテストが赤くなる
決済・メール送信テストのたびに課金され、実際にメールが飛ぶ
データベース起動と初期化が要る。並行実行したテスト同士が同じ行を奪い合う
現在時刻「月末なら締め処理」のテストが月末にしか通らない
乱数・UUID実行のたびに値が変わり、期待値を書けない
異常系「タイムアウトしたとき」を本物で再現できない

見落とされがちなのが最後の行です。[[エラーハンドリングとリトライ設計]] のような異常系こそテストしたいのに、本物のAPIへ「500を返してくれ」とは頼めません。ダブルは速度のためだけでなく、普段起きない状況を意図的に作るための道具でもあります。

5つの分類と使い分け

種類役割
ダミー引数の穴埋めに渡すだけで、実際には使われない
スタブ決められた値を返す。テスト対象への入力を固定する
スパイ呼ばれた回数や引数を記録し、あとから確認する
モック期待する呼ばれ方を先に宣言し、違っていればテストを失敗させる
フェイク簡易だが実際に動く実装(インメモリのリポジトリなど)

実務ではライブラリが全部まとめて「モック」の名前で提供するので、分類名の暗記に意味はありません。効くのは戻り値が欲しいのか、呼ばれたことを確かめたいのかという区別です。検証対象が「結果」ならスタブ、「副作用」ならモックやスパイ。メール送信のように戻り値がない処理は、送られたこと自体を確認します。

// スタブ: 検証するのは戻り値
const rate = { get: async () => 150 }
expect(await toYen(10, rate)).toBe(1500)

// モック/スパイ: 検証するのは「呼ばれ方」
const mailer = { send: vi.fn() }
await register('a@example.com', mailer)
expect(mailer.send).toHaveBeenCalledWith('a@example.com')

差し替えられる形にする(依存性注入)

差し替えたければ、まず差し替えられる形にする必要があります。関数の中で直接 new Date()fetch() を呼んでいると、外から手が出せません。

// Before: 中で時刻を直接読むのでテストできない
function isExpired(token) {
  return token.expiresAt < Date.now()
}

// After: 時刻を引数で受け取る
function isExpired(token, now) {
  return token.expiresAt < now
}
expect(isExpired(token, new Date('2026-01-01').getTime())).toBe(true)

このように依存を外から渡す書き方を依存性注入(DI)と呼びます。引数・コンストラクタ、どの形で渡しても構いません。テストのための小細工に見えますが、[[クリーンコード]] の「責務を1つに絞る」と同じ方向を向いています。時刻を固定するライブラリ機能(fake timers)もありますが、まず引数で受け取れないかを考えます。

モックしすぎると何もテストしていないテストになる

// 何もテストしていないテスト
const repo = { findUser: vi.fn(() => ({ name: '太郎' })) }
expect(getUserName(repo, 1)).toBe('太郎')

getUserNamefindUser の結果を返すだけなら、確かめているのは自分で書いたモックの設定です。よくある失敗は2つあります。

  • ダブルの戻り値が本物とずれる — APIのレスポンス形式が変わってもスタブは変わらないので、テストは緑のまま本番だけ壊れます。最も怖い失敗です
  • 内部の手順をモックで固定する — 「Aを呼んでからBを呼ぶ」まで検証すると、[[リファクタリング]] で中身を整理しただけでテストが赤くなります。動作が変わっていないのに壊れるテストは、安全網ではなく足かせです

線引きの目安はこうです。

対象方針
自分が書いたロジック差し替えない。それがテストしたいもの
外の世界との境界(HTTP・DB・時計・ファイル)差し替える
本物と本当に繋がるか統合テストで、少数の代表ケースだけ本物で確認する

[[REST API]] を呼ぶ処理なら、レスポンスをスタブしたユニットテストで分岐を網羅します。[[CI/CD]] で毎回走らせるのはこちらで、遅い統合テストはマージ時だけ、という運用も一般的です。

初学者向けポイント

  • 迷ったら「実装をわざと壊したら、このテストは赤くなるか?」と自問しましょう。壊しても緑ならモックしすぎです
  • モックライブラリの使い方より先に、依存を引数で受け取る書き方を身につけましょう。ダブルの半分はただのオブジェクトリテラルで足ります
  • 所有していないライブラリの内部を直接モックせず、薄いラッパーを1枚かぶせて差し替えます。更新時に直す箇所が1か所に集まります
  • 同じスタブを何度も書いていると感じたら、フェイクに切り替える合図です

関連技術とのつながり

  • [[自動テスト]] — ユニットテストと統合テストの境界は「依存を差し替えるか」で決まる
  • [[テスト駆動開発]] — 依存を注入できる設計へ導く「使いやすい設計」の具体形
  • [[リファクタリング]] — 内部手順をモックで固定しすぎると、安全網が足かせに変わる
  • [[REST API]] — スタブする代表的な外部依存
  • [[エラーハンドリングとリトライ設計]] — 本物では起こせない失敗をダブルで作り出す
Q: テストで本物の依存をテストダブルに差し替える理由として、本文が挙げていないのはどれ?
- [ ] 外部APIが遅く、相手が落ちるとテストが赤くなるから
- [ ] タイムアウトなどの異常系を本物では再現できないから
- [x] テストコードの行数を必ず短くできるから
解説: 速度・安定性・課金の回避・再現困難な状況を作れることが理由です。行数が短くなることは理由として挙げていません。

Q: 「決められた値を返して、テスト対象への入力を固定する」役割のテストダブルはどれ?
- [x] スタブ
- [ ] ダミー
- [ ] フェイク
解説: スタブは決められた値を返す代役です。ダミーは引数の穴埋め、フェイクは簡易だが実際に動く実装を指します。

Q: 本文が「モックしすぎ」の失敗として挙げているのはどれ?
- [ ] 自分が書いたロジックまで本物のまま動かしてしまう
- [x] 内部の呼び出し手順まで検証し、リファクタリングしただけでテストが赤くなる
- [ ] 統合テストを1本だけに絞ってしまう
解説: 内部手順を固定すると、動作が変わっていないのにテストが壊れ、安全網ではなく足かせになります。