3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MockとStubの違いから学ぶテストダブルの正しい使い方

3
Posted at

はじめに

「このテスト、なんでDBをMockしてないの?」
「ここMockにしたらリファクタリングのたびにテスト壊れるよ」

――こんなレビューコメントのやり取り、見覚えはありませんか。

Mockの使い方は、チーム内で意見が割れやすいテーマです。
「とりあえず全部Mockにする」派と「Mockはなるべく使わない」派がぶつかる場面は珍しくありません。
でもこの対立、実は個人の好みやスキルの差ではなく、
ユニットテストの定義そのものに潜む「解釈の違い」が原因だったりします。

この記事では、MockとStubの正確な定義、Mockの使い方が割れる構造的な原因、
そして「どこにMockを使うべきか」の判断基準を整理します。
対象は、ユニットテストを書いているが、Mockの使い方に迷いがある方です。

TL;DR

  • テストダブルはMock(出力方向の検証)とStub(入力方向の代替)に分かれる。Stubに対してassertしてはいけない
  • Mockの使い方が割れる原因は「隔離」の解釈の違い(London学派 vs Classical学派)
  • Mockを使うべき判断基準はたった1つ: 「その依存のふるまいは、外部アプリケーションから観察可能か?」

MockとStubは別物である

Mock、Stub、Spy、Fake... テストダブルにまつわる用語はたくさんありますが、
実務で押さえるべき区別は実は1つだけです。
この区別がわかっていないと、「Stubにassertする」というアンチパターンに気づけません。

テストダブルの全体像

テストダブル(test double)とは、テスト用に本番コードの代わりに使う偽の依存の総称です。映画のスタントダブル(代役)が語源になっています。

分類としては5種類(dummy, stub, spy, mock, fake)が知られていますが、
実用上は MockグループとStubグループの2つ に集約できます。

┌─────────────────────────────────────────────────┐
│              テストダブルの分類                    │
├─────────────────────────────────────────────────┤
│                                                 │
│  Mockグループ(出力方向 / 検証する)                 │
│    ├ mock ... フレームワークで作るテストダブル        │
│    └ spy  ... 手書きのmock                       │
│                                                 │
│  Stubグループ(入力方向 / 検証しない)               │
│    ├ stub  ... シナリオに応じた戻り値を返す          │
│    ├ dummy ... nullや固定値。引数を埋めるだけ        │
│    └ fake  ... 動作する簡易実装(例: インメモリDB)   │
│                                                 │
└─────────────────────────────────────────────────┘

ここでのポイントは、MockグループとStubグループの違いが
「方向」と「検証するかどうか」 の2点だということです。
次のセクションで詳しく見ていきます。

MockとStubの決定的な違い: 方向

MockとStubを分ける基準は、テスト対象(SUT: System Under Test)から見た呼び出しの方向です。

  • Mock: SUTからの 出力方向 の呼び出しを模倣・検証する。呼び出し先に副作用を起こす操作が対象
  • Stub: SUTへの 入力方向 の呼び出しを模倣する。SUTがデータを取得するための操作が対象
                 ┌─────────┐
   入力方向       │         │      出力方向
   ◀──────────   │   SUT   │  ──────────▶
  Stubで模倣      │         │   Mockで模倣・検証
                 └─────────┘
  例: ユーザー数             例: メール送信
     を取得する                   を実行する

たとえば、「注文が確定したら確認メールを送る」という処理をテストしたいとします。

  • メール送信サービスへの呼び出しは 出力方向 です。SUTが外部に対して副作用を起こしています。この呼び出しを模倣し、「正しく呼ばれたか」を検証するテストダブルが Mock です
  • 一方、「在庫数を取得する」処理への呼び出しは 入力方向 です。SUTがデータを受け取るだけで、呼び出し先に副作用はありません。この呼び出しに固定の戻り値を返すテストダブルが Stub です

この区別は、CQS(Command Query Separation)という設計原則と対応しています。

CQSの分類 特徴 テストダブル
Command(コマンド) 副作用あり、戻り値なし Mock
Query(クエリ) 副作用なし、戻り値あり Stub

Commandは「状態を変えろ」という指示で、Queryは「状態を教えろ」という問い合わせです。
指示にはMock、問い合わせにはStub。
この対応関係を覚えておくと、テストダブルの選択で迷いにくくなります。

「Stubにassertしてはいけない」ルール

MockとStubの区別がなぜ重要かというと、Stubに対してassert(検証)してはいけない という鉄則があるからです。

Stubへの呼び出しは、SUTが最終結果を生み出すための「中間ステップ」にすぎません。
中間ステップを検証するということは、テストが実装詳細に結合するということです。
実装詳細に結合したテストは、リファクタリングしただけで壊れます。

たとえば、「レポートを生成する」機能をテストする場面を考えてみます。

  • レポート生成の内部で GetNumberOfUsers() を呼んでユーザー数を取得する
  • Stubで戻り値を設定するのは問題ない
  • しかし、「GetNumberOfUsers() が1回だけ呼ばれたこと」をassertしたらどうなるか?

ユーザー数の取得方法を変えただけでテストが壊れます。
レポートの内容は正しいのに、テストが赤くなる。
これが「Stubにassertする」アンチパターンの典型です。

Stubに対する検証は、テストの脆さの原因になります。
Stubはあくまで「SUTに入力を与える」ために使い、検証はSUTの出力(最終結果)に対して行いましょう。

よくある混同: Mock (the tool) vs Mock (the test double)

もう1つ、紛らわしいポイントがあります。Mockingライブラリの Mock<T> クラスと、テストダブルとしてのMockは別物です。

Mock<T> はあくまで ツール であり、そのインスタンスが実際にMockなのかStubなのかは、使い方次第で変わります。

使い方 テストダブルの種類
Mock<IEmailGateway>Verify() を使って呼び出しを検証する Mock(テストダブル)
Mock<IDatabase>Setup() を使って戻り値を設定するだけ Stub(テストダブル)

ツールの名前が「Mock」だからといって、
それで作ったインスタンスが全てMock(テストダブル)になるわけではありません。
判断基準は「出力方向の呼び出しを検証しているかどうか」です。

Mockの使い方が割れる構造的な原因

MockとStubの区別は整理できました。しかし、この定義だけでは「どこまでMockを使うか」の問いには答えられません。

チーム内でMockの使い方が割れる根本原因は、実はユニットテストの定義そのものに潜む 「隔離」 という言葉の解釈の違いにあります。

ユニットテストの3つの属性

ユニットテストの定義を掘り下げると、次の3つの属性に行き着きます。

  1. 小さい単位 を検証する
  2. 高速 に実行できる
  3. 隔離された状態 で実行する

1と2は比較的合意が取れています。問題は3の「隔離」です。この一語をどう解釈するかで、ユニットテストに対するアプローチが根本的に変わります。

London学派: SUTを協力者から隔離する

London学派にとっての「隔離」とは、テスト対象のクラスを、そのクラスが依存する全ての協力者から切り離すこと です。

London学派の隔離イメージ

  テストダブル  ─┐
  テストダブル  ─┤──▶ [SUT(1クラス)] ◀── テストダブル
  値オブジェクト ┘                   └── テストダブル

  ※ 値オブジェクト(immutable)だけは本物を使う

値オブジェクト(変更不可能なオブジェクト。例えば数値、文字列、Moneyのような不変の値)
以外の全ての依存をテストダブルに置き換えます。
結果として「ユニット = 1クラス」になり、クラスごとにテストクラスを1対1で作る構造になります。

このアプローチで主張されるメリットは3つあります。

  • テストが失敗したとき、バグの場所が確実に1クラスに絞れる
  • 依存グラフが複雑でもテストが書きやすい
  • テストの粒度が細かく、網羅しやすい

Classical学派: テスト同士を隔離する

Classical学派にとっての「隔離」は全く別の意味です。テストケース同士が互いの実行結果に影響しないようにすること を指します。

テスト間で状態を共有する依存(shared dependency)だけをテストダブルに置き換え、それ以外の依存は本物を使います。

Classical学派の隔離イメージ

  テストA: [SUT] ──▶ [協力者クラス] ──▶ [DB(テストダブル)]
  テストB: [SUT] ──▶ [協力者クラス] ──▶ [DB(テストダブル)]

  ※ 協力者クラスは本物を使う
  ※ DB等の共有依存だけをテストダブルにする

ここで「共有依存(shared dependency)」とは、複数のテストが状態を共有する依存のことです。
典型例はデータベースやファイルシステムです。
テストAがDBにレコードを追加し、テストBがそれを削除すると、
並列実行でテストが不安定になります。これを防ぐためにテストダブルを使います。

一方、テストごとに新しいインスタンスを作れる依存(private dependency)はそのまま本物を使います。
結果として「ユニット = 振る舞いの単位」になり、複数クラスにまたがるテストも普通に書きます。

依存の分類を整理する

ここまでで「共有依存」「値オブジェクト」など断片的に依存の種類が出てきました。学派の対比に入る前に、一度整理しておきます。

依存の分類

  依存(dependency)
  ├── 共有依存(shared dependency)
  │     テスト間で状態を共有する依存
  │     例: DB、ファイルシステム
  │
  └── プライベート依存(private dependency)
        テストごとに独立して使える依存
        ├── mutable(状態が変わる)
        │     例: 協力者クラスのインスタンス
        └── immutable(値オブジェクト)
              例: enum、定数、数値

もう1つ知っておくべき分類として、プロセス外依存(out-of-process dependency) があります。
これはアプリケーションの実行プロセスの外にある依存で、
DB、ファイルシステム、外部APIなどが該当します。

共有依存のほとんどはプロセス外依存ですが、完全には一致しません。
たとえば、テストごとにDockerで新しいDBを起動する場合、
そのDBはプロセス外依存ですが共有依存ではありません。
テスト間で状態を共有しないからです。

2学派の対比

ここまでの内容を対比表にまとめます。

観点 London学派 Classical学派
何を隔離するか SUT(テスト対象のコード) テストケース同士
「ユニット」とは 1つのクラス 振る舞いの単位(複数クラス可)
テストダブルの対象 immutable以外の全依存 共有依存のみ
テスト構造 クラスとテストクラスが1対1 振る舞いの単位に対応
テスト失敗時 バグの場所が1クラスに絞れる 連鎖的に複数テストが落ちることがある

つまり、「Mockをどこまで使うか」が割れるのは、
2つの学派が「隔離」の意味を全く違うものとして捉えているからです。
London学派は「ほぼ全ての依存にテストダブルを使う」、
Classical学派は「共有依存にしかテストダブルを使わない」。
この違いを認識していないと、レビューで永遠に平行線をたどることになります。

London学派の「全部Mock」が招く問題

London学派のアプローチは一見すると合理的に見えます。テスト失敗時にバグの場所が特定しやすい、複雑な依存グラフでもテストが書ける。メリットはあります。

しかし、このアプローチにはMock多用がもたらす本質的な問題が3つあります。

「1クラス = 1テスト」の罠

テストは「コードの単位」ではなく 「振る舞いの単位」 を検証するものです。

たとえば、犬の「足」「頭」「尻尾」をそれぞれ個別にテストしても、
「犬が飼い主のところに来る」という振る舞いが正しく動くかはわかりません。
1クラスだけを切り出してテストするのはこれと同じで、
粒度が細かすぎるテストは何を検証しているのか読み手にとって不明瞭になります。

テストは「ビジネスの問題を解決するストーリー」を語るべきものです。そのストーリーが1クラスに閉じている必要はありません。

内部通信をMockで検証する危険性

ここがLondon学派の最も深刻な問題です。

アプリケーション内のクラス間通信(intra-system communications)と、
アプリケーション間の通信(inter-system communications)は、性質が全く異なります。

┌──────────────────────────────────────────────────┐
│  アプリケーション                                   │
│                                                  │
│    [Controller]                                  │
│        │                                         │
│        ├──▶ [Customer] ──▶ [Store]               │
│        │      intra-system(内部通信)             │
│        │      = 実装詳細                           │
│        │                                         │
│        └──▶ [EmailGateway] ──▶ ✉ SMTPサーバー     │
│              inter-system(外部通信)              │
│              = 観察可能なふるまい                   │
│                                                  │
└──────────────────────────────────────────────────┘
  • 内部通信(intra-system): クラスAがクラスBのメソッドをどう呼ぶかは、リファクタリングで変わりうる実装詳細です
  • 外部通信(inter-system): 外部アプリケーションとの通信パターンは、後方互換性のために維持する必要があるふるまいです

London学派はこの2つを区別しません。
内部通信も外部通信も同じようにMockで検証します。
その結果、内部のクラス間通信に対してMockで
「この順番で、このメソッドが呼ばれたこと」を検証するテストを大量に書くことになります。

これが over-specification(過剰な仕様化) です。
実装詳細にテストが結合してしまい、内部のリファクタリングをするたびにテストが壊れます。
テストの意味は変わっていないのに、赤くなる。
これではリファクタリングの安全網としてのテストが機能しません。

先ほど触れた「Stubにassertしてはいけない」ルールも、over-specificationの一種です。
Stubへの呼び出しは実装詳細なので、それを検証すること自体がover-specificationにあたります。

「テストが書きやすい」は設計の問題を隠す

ここまではMockで「何を検証するか」の問題でした。もう1つ、Mockが隠してしまう別種の問題があります。コード設計そのものの問題です。

「依存グラフが複雑でテストが書けない」とき、
Mockで依存を切ればテストは書けるようになります。
しかし、それは問題を解決したのではなく、問題を見えなくしただけです。

テストが書きにくいことは、コード設計に問題がある良い指標です。
依存グラフが複雑すぎるなら、Mockで切り離す前にその設計自体を見直す余地があります。
Mockで覆い隠してしまうと、設計改善のきっかけを失ってしまいます。

Mockを使うべき唯一の判断基準

London学派の問題が見えてきたところで、
ではClassical学派の立場に立てばMockは不要なのか? というと、そうではありません。
Classical学派のアプローチでも、Mockが正当に必要な場面があります。

その境界線を正確に引きます。

全てのプロセス外依存をMockすべきではない

Classical学派は共有依存をテストダブルに置き換えます。共有依存の多くはプロセス外依存(DB、ファイルシステムなど)です。

しかし、プロセス外依存にも2種類あります。

種類 定義
managed dependency(管理された依存) 自分のアプリケーションだけがアクセスする アプリ専用のDB
unmanaged dependency(管理されていない依存) 外部アプリケーションからも観察可能 SMTPサーバー、メッセージバス、外部API

この区別こそが、Mockを使うかどうかの判断基準です。

managed dependency にMockを使わない理由

アプリケーション専用のDBを考えてみましょう。このDBにアクセスするのは自分のアプリケーションだけで、外部から直接覗く人はいません。

つまり、アプリケーションとDBは 一体のシステム として扱えます。
DBとの通信パターン(どのテーブルにどんなクエリを投げるか)は実装詳細です。
テーブルを分割したり、クエリを最適化したりしても、外部から見た振る舞いは変わりません。

この通信パターンをMockで固定してしまうと、
DB周りのリファクタリングのたびにテストが壊れます。
後方互換性を維持する必要がない相手に対して、Mockで通信パターンを検証する理由はありません。

では、DB関連の処理はどうテストするのか? それは統合テストで実際のDB(またはその同等物)を使って検証します。ユニットテストでMockを使う対象ではないということです。

unmanaged dependency にMockを使う理由

一方、外部アプリケーションから観察可能な副作用を持つ依存は事情が異なります。

たとえば、「購入が成功したら確認メールを送る」という要件があるとします。
メール送信はビジネス上の要件であり、外部(ユーザーやメールサーバー)から期待される振る舞いです。
この通信パターンは、リファクタリングしても維持されるべきものです。

また、外部システム(SMTPサーバーやサードパーティAPI)は
自分のアプリケーションとは異なるデプロイサイクルで動いていたり、
そもそも制御できなかったりします。
後方互換性の維持が必要な相手には、Mockを使って通信パターンを検証するのが正当です。

判断フローチャート

ここまでの判断基準を、1つのフローにまとめます。

最終的な判断は、以下の対比表に集約されます。

依存の種類 テストでの扱い
プライベート依存(immutable) 値オブジェクト、定数 そのまま使う
プライベート依存(mutable) インメモリの協力者クラス そのまま使う
共有依存(managed) アプリ専用DB 統合テストで実物を使う
共有依存(unmanaged) SMTPサーバー、メッセージバス Mockで通信パターンを検証する

London学派のアプローチでは、プライベート依存(mutable)もテストダブルに置き換えます。この記事で整理した判断基準は、Classical学派寄りの考え方に基づいています。

まとめ

この記事では、Mockの正しい使い方を「定義 → 原因 → 判断基準」の順で整理しました。

まず、 MockとStubは方向が違います。
Stubは入力方向で検証しない。Mockは出力方向で検証する。
この区別を間違えると、実装詳細に結合した脆いテストを書いてしまいます。

次に、 Mockの使い方が割れるのは「隔離」の解釈の違いが原因です。
London学派はSUTを協力者から隔離し、ほぼ全ての依存にテストダブルを使います。
Classical学派はテスト同士を隔離し、共有依存にのみテストダブルを使います。

そして、 London学派のアプローチは内部通信をMockで固定してしまう問題があります。
クラス間の内部通信は実装詳細であり、それをMockで検証するとリファクタリング耐性を失います。

最後に、Mockを使うべき判断基準はたった1つ です。

その依存のふるまいは、外部アプリケーションから観察可能か?

YESなら、Mockで通信パターンを検証する。NOなら、Mockは使わない。

Mockは「テストを楽にする道具」ではなく、
「アプリケーション境界の契約を検証する道具」です。
この基準を持っておくだけで、「ここにMockを使うべきか?」という判断がブレなくなるはずです。

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?