はじめに
「機能を追加してコードを綺麗にリファクタリングしただけなのに、なぜかテストが大量に落ちる……」
「画面の動きやAPIのレスポンスは問題ないのに、テストをパスさせるためだけにモックの定義を延々と修正している……」
テストを書いているエンジニアなら、一度はこのような「モックの泥沼」にはまった経験があるのではないでしょうか?
現場で「モック(テストダブル)」の使い方を誤ると、どれほど綺麗なコードを書いてもテストが足を引っ張る「負債」に変わってしまいます。
この記事では、何でもモック化する「ロンドン学派」と、モックを極力使わない「古典学派」という2つの思想を対比させます。著書『単体テストの考え方/使い方』では結論として「古典学派」を推奨しています。
本書がなぜ古典学派を推すのか、その理由に触れながら、「モックをどこにどう使うべきか」を判断できるよう解説します。
モックを単なる「テストを書きやすくするための道具」から「適切な境界で正しく使いこなすツール」に変えて、変更に強いテストコードを書いていきましょう。
読了時間の目安: 約 10 分
参考図書: 『単体テストの考え方/使い方』(Vladimir Khorikov 著)
1. なぜモックの使い方を誤るとテストが負債になるのか
単体テストの世界には、モックに対する考え方の違いから生まれた2つの流派が存在します。モックを積極的に活用する「ロンドン学派」と、モックを極力使わない「古典学派」です。この対比を通じて、モックをどの境界に、どう当てるべきかを整理していきます。
良いテストを測る物差し:「リファクタリング耐性」
2つの流派を比べる前に、「良いテストとは何か」を測る物差しを押さえておきます。『単体テストの考え方/使い方』では、テストの価値を次の4つの観点で評価します。
| 観点 | 意味 |
|---|---|
| 退行(リグレッション)に対する保護 | バグが入り込んだとき、テストがそれを見つけてくれる度合い |
| リファクタリング耐性 | 外から見た振る舞いを変えずに内部実装を書き換えたとき、テストが失敗しない度合い |
| 迅速なフィードバック | テストが速く終わる度合い |
| 保守のしやすさ | テストを読んで理解し、動かし続けるのが楽な度合い |
この記事で重要になるのは、2番目の「リファクタリング耐性」です。冒頭で挙げた「リファクタリングしただけなのにテストが大量に落ちる」状態は、リファクタリング耐性が低いテストで起こります。
このとき、アプリの振る舞いは正しいのにテストが失敗します。この誤警報を偽陽性(と呼びます。偽陽性が増えると、次の問題が起きます。
- 本物のバグで失敗したのか、リファクタリングで失敗しただけなのかを、毎回調べ直すことになる。
- 失敗に慣れてしまい、本物のバグを知らせる失敗も見過ごしやすくなる。
- テストを直す手間がかかるため、リファクタリングそのものをためらうようになる。
そして、偽陽性の主な原因は、テストが「処理の結果」ではなく「内部の実装の詳細」を検証していることです。モックは、この実装の詳細を検証する道具として使われやすいため、使い方を誤ると偽陽性の温床になります。この視点で、次の2つの流派を見ていきます。
2. ロンドン学派(モック中心主義)とは
まず1つ目が、モックを積極的に活用する「ロンドン学派」です。
考え方:依存オブジェクトをすべてモック化し、「どう呼び出したか(やり取り)」をチェックする
ロンドン学派では、テスト対象のクラスが依存する「他のすべてのオブジェクト(不変な値オブジェクトを除く)」をモック(テストダブル)に置き換えます。
そして、「テスト対象が、依存先にどんな引数で、何回、どの順番でメソッドを呼び出したか」をアサートして検証します。
ロンドン学派のコード例
Pythonの unittest.mock を使い、注文処理(OrderService)をテストするコードを見てみましょう。
# --- テスト対象のコード ---
class OrderService:
def __init__(self, inventory_service, email_service):
self.inventory_service = inventory_service
self.email_service = email_service
def checkout(self, user_email: str, product_id: str, quantity: int, unit_price: int, coupon_code: str) -> int:
# 1. クーポンを適用した合計金額を計算する(ビジネスロジック)
total_price = self._apply_coupon(unit_price * quantity, coupon_code)
# 2. 在庫を減らす(ドメインの状態変更)
self.inventory_service.reduce_stock(product_id, quantity)
# 3. 確認メールを送信する(外部I/O)
self.email_service.send_confirmation(user_email, product_id, total_price)
return total_price
def _apply_coupon(self, subtotal: int, coupon_code: str) -> int:
if coupon_code == "WELCOME500":
return max(subtotal - 500, 0)
return subtotal
# --- ロンドン学派的なテストコード ---
from unittest.mock import MagicMock
def test_checkout_london_style():
# 準備:社内オブジェクトも外部サービスも「すべて」モックにする
mock_inventory = MagicMock()
mock_email = MagicMock()
service = OrderService(mock_inventory, mock_email)
# 実行
total = service.checkout("user@example.com", "item_101", 2, 5000, "WELCOME500")
# 検証:各メソッド呼び出し(プロセス)を監視・チェックする
mock_inventory.reduce_stock.assert_called_once_with("item_101", 2)
mock_email.send_confirmation.assert_called_once_with("user@example.com", "item_101", 9500)
assert total == 9500
このテストは一見すると何の問題もなく見えますが、リファクタリング耐性に問題があります。
なぜこのテストは「リファクタリング耐性」が低いのか?
具体的な場面で確かめてみます。
もし後から「メール送信の仕組みを整理して、他の通知(LINEやSMSなど)とも共通で使える『通知サービス(notification_service)』経由で送る方式に変えよう」とリファクタリングした場合、どうなるでしょうか?
【リファクタリング前の内部処理】
checkout() ──► inventory_service.reduce_stock()
──► email_service.send_confirmation() <-- テストが「この特定の呼び出し」をチェックしている
【リファクタリング後の内部処理】
checkout() ──► inventory_service.reduce_stock()
──► notification_service.send_notice() <-- 通知サービス経由でメールを送るように変更
アプリとしては「注文完了のメールをユーザーに届ける」という仕様(結果)は何も変わっていません。ユーザーにも全く同じメールが届きます。
しかし、上記のロンドン学派のテストは mock_email.send_confirmation.assert_called_once_with(...) という内部の実装の詳細を直接アサートしているため、内部の仕組みを変えただけで即座にテストが失敗(偽陽性)してしまうのです。
ロンドン学派のメリット・デメリット
-
メリット
- 依存先がまだ完成していなくても、モックを作ることで単体テストが書ける。
- テスト対象だけを「完全隔離」してテストできるため、失敗した時に原因の特定が容易。
-
デメリット
- リファクタリング耐性が低く、壊れやすいテストになりやすい。
3. 古典学派(状態ベース)とは
もう1つの流派であり、『単体テストの考え方/使い方』の著者も推奨するのが「古典学派(状態ベース)」です。
考え方:モックは極力使わず「最終的な結果(状態や返り値)」を検証する
古典学派では、モックは極力使わず、可能な限り本物のオブジェクト(ドメインモデル)同士を組み合わせてテストを行います。
検証するのは「どうやって処理されたか(相互作用)」ではなく、「処理が終わった結果、オブジェクトの状態や返り値がどう変わったか(状態検証)」です。
古典学派のコード例
本物のドメインオブジェクト(Order や Inventory)を連携させ、状態変化をアサートするコードを見てみましょう。Inventory は商品ごとの在庫数をメモリ上で管理するだけの、ごく単純なドメインオブジェクトです(外部DBやAPIとは通信しません)。
# --- 在庫を管理するドメインオブジェクト ---
class Inventory:
def __init__(self, stock: dict):
self._stock = stock
def reduce_stock(self, product_id: str, quantity: int):
self._stock[product_id] -= quantity
def get_stock(self, product_id: str) -> int:
return self._stock[product_id]
# --- テスト対象のコード ---
class OrderService:
def __init__(self, inventory_service, email_service):
self.inventory_service = inventory_service
self.email_service = email_service
def checkout(self, user_email: str, product_id: str, quantity: int, unit_price: int, coupon_code: str) -> int:
# 1. クーポンを適用した合計金額を計算する(ビジネスロジック)
total_price = self._apply_coupon(unit_price * quantity, coupon_code)
# 2. 在庫を減らす(ドメインの状態変更)
self.inventory_service.reduce_stock(product_id, quantity)
# 3. 確認メールを送信する(外部I/O)
self.email_service.send_confirmation(user_email, product_id, total_price)
return total_price
def _apply_coupon(self, subtotal: int, coupon_code: str) -> int:
if coupon_code == "WELCOME500":
return max(subtotal - 500, 0)
return subtotal
# --- 古典学派的なテストコード ---
from unittest.mock import MagicMock
def test_checkout_classic_style():
# 準備:
# - 在庫(ドメイン)は「本物」を使う
# - メール(外部への出口。ここでは email_service が SMTP に直接つながる薄い窓口と想定)だけ「モック」にする
real_inventory = Inventory({"item_101": 10})
mock_email = MagicMock()
service = OrderService(real_inventory, mock_email)
# 実行
total = service.checkout("user@example.com", "item_101", 2, 5000, "WELCOME500")
# 検証:
# 1. 在庫は「本物の状態変化(10 -> 8)」でチェック
assert real_inventory.get_stock("item_101") == 8
# 2. クーポン適用後の合計金額は「返り値」でチェック
assert total == 9500
# 3. メールはシステム外への副作用(プロセス外依存)なので、境界での呼び出しをチェック
mock_email.send_confirmation.assert_called_once_with("user@example.com", "item_101", 9500)
なぜこのテストは「リファクタリング耐性」が高いのか?
このテストの最大のポイントは、「社内ロジック(在庫管理)にはモックを使わず本物の状態を検証し、制御不能な外部通信(メール)だけを境界でモック化していること」です。
-
社内ドメイン(
Inventory)に対するリファクタリング耐性
在庫を減らす内部処理をどう書き換えても(関数を小分けにしようが、内部のデータ構造を変えようが)、「処理完了後に本物の在庫が8個に減っている」という最終結果(状態)だけを検証しています。そのため、社内コードの実装変更によってテストが壊れる(偽陽性が出る)心配がありません。 -
ビジネスロジック(クーポン計算)に対する保証
_apply_couponの中身を「if文で分岐」から「クーポン種別ごとのテーブル参照」に書き換えたとしても、検証しているのはcheckout()の返り値(9500円)だけです。内部のアルゴリズムをどう変えても、最終的な計算結果さえ合っていればテストは通り続けます。 -
外部通信(
email_service)に対する適切な保護
メール送信は外部システムへの副作用(プロセス外依存)であるため、実際にメールが送信されたりネットワークエラーでテストが揺らいだりしないよう、外部との境界(mock_email)だけで呼び出しをチェックしています。社内ロジックの呼び出し手順まで細かくアサートするロンドン学派とは異なり、モックでのアサートを外部境界だけに留めているためテストの壊れにくさを維持できます。
補足:モックを当てるのは「システムの一番外側の境界」
ここで、「古典学派のテストも mock_email.send_confirmation の呼び出しを検証している。これでは2章と同じように、通知サービス経由に変えたら壊れるのではないか?」
と思われるかもしれません。
これは、モックをどの位置に置くかで決まります。
- このコード例の
email_serviceは、SMTP などの外部メールサーバーに直接つながる、薄い窓口だと想定しています。つまり、email_serviceがシステムの一番外側の境界そのものです。この位置にモックを当てている限り、テストが確認しているのは「外部へメールが出ていくこと」で、これは仕様の一部です。 - 一方、
email_serviceの内側に「通知サービス」などの社内オブジェクトが増え、email_serviceがその1つになった場合は、話が変わります。email_serviceはもう外側の境界ではなく、社内の部品です。この場合のモックは、さらに外側にある本当の出口(SMTPクライアントなど)に移します。
つまり、リファクタリングで社内の呼び出し経路が変わっても、一番外側の境界のモックなら影響を受けません。2章のテストが壊れたのは、社内の協力オブジェクトの呼び出しまで、モックで検証していたためです。
このように、「ドメインの状態変化」と「外部I/Oの境界」を明確に切り分けてテストを書くことで、偽陽性を極力排除しつつ、変更に強い強固なテストを実現できます。
古典学派のメリット・デメリット
-
メリット
- リファクタリング耐性が高く、コードの改修に強い。
- 「実装の詳細」ではなく「ビジネス仕様(外から見える振る舞い)」をそのまま表現したテストになる。
- 本物のオブジェクト同士を連携させるため、モックが実際の挙動と乖離していることによる見落とし(本来起きるはずの不具合をモックが隠してしまう問題)を防ぎやすい。
-
デメリット
- 依存関係が複雑な場合、準備(Arrange)のセットアップコードがやや大きくなりやすい。
- 本物のオブジェクト同士をつなぐため、1つの不具合で複数のテストが同時に失敗することがあり、ロンドン学派に比べて原因の特定に手間がかかりやすい。
4. 比較してわかる「モックの正しい使い方・選び方」
「じゃあモックは完全に悪なのか?一切使ってはいけないのか?」と思われるかもしれません。
結論は 「モックには正しく使うべき限定的な出番がある」 です。
基本姿勢:古典学派(状態検証)をデフォルトにする
実務でのテスト方針としては、「原則として古典学派(本物のオブジェクト同士を組み合わせる状態検証)」をベースにします。
社内のビジネスロジックやドメインモデルの相互作用は、モック化してはいけません。本物のインスタンス同士を直接連携させ、「どう動いたか(実装の詳細)」ではなく「どうなったか(最終的な状態・結果)」だけをアサートします。
モック(テストダブル)を使うべき「唯一の例外」とは?
モックを使うべき対象は、「プロセス外依存(制御不能な外部I/O)」のうち、外部のシステムから観察できる副作用を持つものに限られます。
【モック化してはいけないもの】(ドメインロジック / 制御可能な領域)
× 社内のドメインオブジェクト同士(User, Order, Item)
× メモリ内で完結する純粋な計算・ビジネスルール
× ローカル環境で制御可能なデータベース
【モック化すべき唯一の例外】(プロセス外依存 / 制御不能な外部I/O)
○ サードパーティの外部決済API(Stripe, PayPal など)
○ 外部メール送信サービス(SendGrid, Amazon SES など)
○ 外部メッセージキュー、サードパーティのWeb Hook通知
外部APIやメール送信だけをモックにする理由は、シンプルです。
テストのたびに本物の決済処理が走って課金されたり、ユーザーへテストメールが飛ばされたりする「意図しない副作用」を防ぐため。そして、通信エラーや遅延で「成功したり失敗したりする不安定なテスト」を排除するためです。
制御不能な外部境界だけをピンポイントでモック化することで、初めて「いつでも安心して一瞬で実行できるテスト」が完成します。
モックを当てる前に:まず「ビジネスロジックとI/Oの分離」を検討する
ここまでは「どこにモックを当てるべきか」という境界の話をしてきましたが、実はモックを検討する前に、もう一段階手前でやるべきことがあります。それが「ビジネスロジックとI/O(プロセス外依存)をプロダクトコード側で分離できないか」を先に考えることです。
例えば、先ほどの OrderService._apply_coupon は外部I/Oに一切依存しない、純粋なビジネスロジックです。これを独立したクラスに切り出すと、どう変わるか見てみましょう。
# --- 分離後:外部I/Oに依存しない、純粋なビジネスロジックだけを持つクラス ---
class CouponCalculator:
def apply(self, subtotal: int, coupon_code: str) -> int:
if coupon_code == "WELCOME500":
return max(subtotal - 500, 0)
return subtotal
# --- 分離後:I/Oとのオーケストレーションに専念するクラス ---
class OrderService:
def __init__(self, inventory_service, email_service, coupon_calculator):
self.inventory_service = inventory_service
self.email_service = email_service
self.coupon_calculator = coupon_calculator
def checkout(self, user_email: str, product_id: str, quantity: int, unit_price: int, coupon_code: str) -> int:
total_price = self.coupon_calculator.apply(unit_price * quantity, coupon_code)
self.inventory_service.reduce_stock(product_id, quantity)
self.email_service.send_confirmation(user_email, product_id, total_price)
return total_price
# --- CouponCalculator単体のテスト:モックが一切不要 ---
def test_coupon_calculator_applies_discount():
calculator = CouponCalculator()
assert calculator.apply(10000, "WELCOME500") == 9500
CouponCalculator は「引数を渡して返り値をアサートするだけ」で検証が完結し、モックは一切登場しません。OrderService 側のテストも、クーポン計算の分岐が増えようが変わらず、相変わらず mock_email だけをモック化すればよい状態を保てます。分離によって「テストすべき対象」と「モックが必要な境界」がより明確になったことが分かります。
クラスを役割ごとに分けておけば、ビジネスロジック側のテストはそもそもモックが不要になります。モックの要否を判断する優先順位は、次のとおりです。
-
最優先:プロダクトコードを分離する
ビジネスロジック(計算・判定などの純粋処理)と、I/O(外部API・メール送信・DBアクセスなど)が1つのクラスやメソッドに混在している場合、まずはプロダクトコード側を見直し、両者を別のクラス・関数に切り出せないか検討します(上記のCouponCalculatorがその例です)。 -
次善策:分離してもなお残るI/Oにだけモックを当てる
分離を行った後でも、メール送信や決済処理のように「本質的にプロセス外依存であるもの」は残ります。上記のOrderServiceの例でmock_emailだけをモック化していたのは、この「分離した後に残ったI/O」に対する処置です。 -
最終手段:分離が困難な場合に限り、やむを得ずモックで対応する
レガシーコードなどですでにビジネスロジックとI/Oが密結合しており、今すぐの改修コストが見合わない場合は、無理にリファクタリングせず、一時的にモックで依存を切り離してテストを成立させることもあります。ただしこれはあくまで次善の策であり、将来的にはプロダクトコード側の分離を目指すべき技術的負債として認識しておくことが重要です。
ポイント
モックを当てる箇所が増えてきたら、それは「このクラスはビジネスロジックとI/Oを分離すべきサインかもしれない」と捉え、まずプロダクトコード自体の設計を見直すことを優先してください。モックはテストを書くための道具である前に、設計の歪みを可視化するシグナルでもあります。
5. まとめ
今回は、単体テストにおける2つの流派「ロンドン学派」と「古典学派」を通して、モックの正しい使い方・選び方を解説しました。
本記事の要点
-
モックの過剰使用は「偽陽性」を発生させる
内部の呼び出し手順(プロセス)をモックで過剰にチェックすると、リファクタリング耐性が低下し、テストが開発の足を引っ張る「負債」になってしまいます。 -
「古典学派(状態ベース)」をテストの基本にする
モックは極力使わず、本物のオブジェクト同士を協調させ、実行後の「最終的な結果(状態や返り値)」を検証します。 -
モックを使うのは「プロセス外依存(制御不能なI/O)」だけに限定する
サードパーティAPIやメール送信など、テスト環境で制御できない「システムの境界外」だけを例外的にモック化します。 -
モックを当てる前に、まず「分離」を検討する
ビジネスロジックとI/Oが混在している場合は、プロダクトコードを修正して両者を分離することを優先します。分離してもなお残るI/Oにだけモックを当てるのが正しい順序であり、分離が困難な場合に限り、やむを得ずモックで対応します。