テストダブル(モック・スタブ)
テストダブルとは
テストダブル(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('太郎')
getUserName が findUser の結果を返すだけなら、確かめているのは自分で書いたモックの設定です。よくある失敗は2つあります。
- ダブルの戻り値が本物とずれる — APIのレスポンス形式が変わってもスタブは変わらないので、テストは緑のまま本番だけ壊れます。最も怖い失敗です
- 内部の手順をモックで固定する — 「Aを呼んでからBを呼ぶ」まで検証すると、[[リファクタリング]] で中身を整理しただけでテストが赤くなります。動作が変わっていないのに壊れるテストは、安全網ではなく足かせです
線引きの目安はこうです。
| 対象 | 方針 |
|---|---|
| 自分が書いたロジック | 差し替えない。それがテストしたいもの |
| 外の世界との境界(HTTP・DB・時計・ファイル) | 差し替える |
| 本物と本当に繋がるか | 統合テストで、少数の代表ケースだけ本物で確認する |
[[REST API]] を呼ぶ処理なら、レスポンスをスタブしたユニットテストで分岐を網羅します。[[CI/CD]] で毎回走らせるのはこちらで、遅い統合テストはマージ時だけ、という運用も一般的です。
初学者向けポイント
- 迷ったら「実装をわざと壊したら、このテストは赤くなるか?」と自問しましょう。壊しても緑ならモックしすぎです
- モックライブラリの使い方より先に、依存を引数で受け取る書き方を身につけましょう。ダブルの半分はただのオブジェクトリテラルで足ります
- 所有していないライブラリの内部を直接モックせず、薄いラッパーを1枚かぶせて差し替えます。更新時に直す箇所が1か所に集まります
- 同じスタブを何度も書いていると感じたら、フェイクに切り替える合図です
関連技術とのつながり
- [[自動テスト]] — ユニットテストと統合テストの境界は「依存を差し替えるか」で決まる
- [[テスト駆動開発]] — 依存を注入できる設計へ導く「使いやすい設計」の具体形
- [[リファクタリング]] — 内部手順をモックで固定しすぎると、安全網が足かせに変わる
- [[REST API]] — スタブする代表的な外部依存
- [[エラーハンドリングとリトライ設計]] — 本物では起こせない失敗をダブルで作り出す
Q: テストで本物の依存をテストダブルに差し替える理由として、本文が挙げていないのはどれ?
- [ ] 外部APIが遅く、相手が落ちるとテストが赤くなるから
- [ ] タイムアウトなどの異常系を本物では再現できないから
- [x] テストコードの行数を必ず短くできるから
解説: 速度・安定性・課金の回避・再現困難な状況を作れることが理由です。行数が短くなることは理由として挙げていません。
Q: 「決められた値を返して、テスト対象への入力を固定する」役割のテストダブルはどれ?
- [x] スタブ
- [ ] ダミー
- [ ] フェイク
解説: スタブは決められた値を返す代役です。ダミーは引数の穴埋め、フェイクは簡易だが実際に動く実装を指します。
Q: 本文が「モックしすぎ」の失敗として挙げているのはどれ?
- [ ] 自分が書いたロジックまで本物のまま動かしてしまう
- [x] 内部の呼び出し手順まで検証し、リファクタリングしただけでテストが赤くなる
- [ ] 統合テストを1本だけに絞ってしまう
解説: 内部手順を固定すると、動作が変わっていないのにテストが壊れ、安全網ではなく足かせになります。