ソフトウェア開発者の皆さん、「良いテスト」書いていますか?
コードの品質を支える自動テスト、その書き方には二つの大きな流派が存在します。
「デトロイト派(古典派)」 と 「ロンドン派(モック主義)」 です。
界隈では「どちらのスタイルが優れているか」だったり、「うちのPrjはXXX派だけど、もう一方の方が良くない?」という議論が時折巻き起こります。
この記事では二つの流派の本質を理解し、あなたのコードに最適なテスト戦略を描くためのヒントとなれば幸いです。
そもそも「デトロイト派」と「ロンドン派」ってなに?
一言で表すとこんな感じです。
- デトロイト派: モック化を極力おこなわずにテストを記述する考え方
- ロンドン派: モック化を積極的におこなってテストを記述する考え方
これだけだと分かりにくいと思うので、具体例を出してみます。
レストランで考える各派閥の「テスト」
二つの流派の違いを、レストランのシェフと提供される料理を例に考えてみましょう。
デトロイト派(古典派)- 料理が美味しいことをテストする
- 「注文された『牛丼』が、レシピ通り美味しくできているか?」を検証します
- つまり、最終的な料理(状態)を評価 します
- 最終的に出てきた料理が、期待通りの美味しさであれば成功とみなします
ロンドン派(モック主義)- 調理過程が正しいことをテストする
- 「シェフは、アシスタントに『牛肉を煮込んで』と正しく指示し、精肉担当に『牛肉を仕入れて』と適切に依頼したか?」を検証します
- つまり、シェフと協力者との やりとり(振る舞い)を評価 します
- シェフが各担当者に対し、手順書通りの「指示」を正しく出せていれば成功と見なします
プログラムベースで考える各派閥の「テスト」
デトロイト派(古典派)- 最終出力が正しいことをテストする
デトロイト派は、極力モックを使用しない考え方です。
テスト対象とその協力者(依存する他のクラス)をできるだけ本物に近い形で動かし、メソッド実行後の状態が期待通りか検証します。
# spec/user_service_detroit_spec.rb
RSpec.describe UserService do
describe '#register' do
# --- デトロイト派(古典派)のテスト ---
# 協力オブジェクト(Database)も本物に近いものを使ってテストする
context 'デトロイト派のスタイル' do
let(:database) { Database.new }
let(:user_service) { UserService.new(database) }
it 'ユーザーを登録すると、データベースにそのユーザーが保存されること' do
# 1. 実行
user_service.register('Taro Yamada')
# 2. 【状態の検証】
# 実行後の「結果」として、データベース内にユーザーが保存されているかを確認します。
saved_user = database.find_by_name('Taro Yamada')
expect(saved_user).not_to be_nil
expect(saved_user.name).to eq('Taro Yamada')
end
end
end
end
ロンドン派(モック主義)- 演算過程が正しく通っていることをテストする
ロンドン派は、テスト対象が依存するオブジェクトを 「モック」と呼ばれる偽物に積極的に差し替えます。
そして、テスト対象がそのモックを正しく使っているか(メソッドを正しく呼び出しているか)を検証します。
# spec/user_service_london_spec.rb
RSpec.describe UserService do
describe '#register' do
# --- ロンドン派(モック主義)のテスト ---
# 協力オブジェクト(Database)をモックに差し替えてテストする
context 'ロンドン派のスタイル' do
# `double` を使って協力者であるDBのモック(偽物)を作成
let(:database_mock) { double('Database') }
let(:user_service) { UserService.new(database_mock) }
it '正しいユーザーオブジェクトを引数にして、DBのsaveメソッドを呼び出すこと' do
# 1. 【振る舞いの期待値を設定】
# 実行前に「`database_mock` の `save` メソッドが、
# nameが'Taro Yamada'のUserオブジェクトを引数に1回呼ばれるはずだ」
# という期待を表明します。
expected_user = User.new('Taro Yamada')
expect(database_mock).to receive(:save).with(expected_user).once
# 2. 実行
user_service.register('Taro Yamada')
# 3. 検証 (RSpecでは`expect(...).to receive`の行で暗黙的に行われる)
end
end
end
end
まとめると
どちらのスタイルにも、得意なことと不得意なことがあります。
メリット (どういう時に嬉しいか) | デメリット (どういう時に困るか) | |
---|---|---|
デトロイト派 (古典派) |
・リファクタリングに強い: 内部実装を変えてもテストが壊れにくい。 ・信頼性が高い: 実際に近い構成で動くことを保証できる。 |
・実行が遅い: 多くのオブジェクトを動かすため。 ・失敗時の原因特定が難しい: どこで問題が起きたか分かりにくい。 |
ロンドン派 (モック主義) |
・高速で分離性が高い: テストしたい箇所に集中できる。 ・設計へのフィードバック: テストの書きにくさから設計の問題に気づける。 ・外部要因を排除できる: DBやAPIを気にせずテストできる。 |
・実装と密結合しやすい: リファクタリングでテストが壊れやすい。 ・過剰なモック: モックの準備が複雑になりがち。 |
上記を踏まえて自分はどう考えているのか
自分個人の意見としては デトロイト派(古典派)をベース としつつ、一部のテストにのみ 部分的にモック化 の考えを取り入れるようにしてます。
なぜならば、アプリケーションの挙動として 「最終出力が期待通りである(※最初の例で言うと「最終的な味が担保されていること」)」 が一番大事であり、 「過程は変化しうるもの」 と考えているためです。
ゆえにモックへの差し替えは極力避け、本番環境の挙動との乖離やリファクタリングの阻害要因を産まないように意識しています。
しかし、どうしても難しいところだったり、詳細な挙動を見たいところだけモックを取り入れたテストを導入しています。参考までにご紹介します。
モック化を検討するのはどこか
例外の再現が困難なケース
例外挙動のテストはまさにモック化の検討が行われるケースですね。
ですが私たちは例外挙動のテストにおけるモック化は「例外再現が困難なケース」に限定させるように意識しています。
(外的リソースで例外をコントロールできない場合など)
例えば「存在しないURLにアクセスした場合の例外」や「認証していない場合の例外」などは再現が容易なものについては、出来る限りモック化せずにありのままでテストを記述するようにしています。
AIなど、同じInputでも返答が変化しうる外的リソース
同じInputでも回答が変化しうる外的リソースのテストを記述する時はモック化を検討します。Geminiなどの生成AI系がそうですね。
我々のプロダクトはAIがコアシステムに入り込んでいるので、どうしてもテストを書かざるを得ません。
「AIがこう返してきたら」という部分をモックで書いて、このメソッドは「こう返すはず」という感じの書き方をするイメージで、細かい挙動はこれで検証しています。
しかし、モック化していないテストもあえて書いてます。
AIの仕様変更により意図しない最終成果物が出てくる可能性があるためです。
匙加減が難しいですが「これを渡したら、これぐらいの文字量は返してくるだろう」「このワードを含んだものは返してくるだろう」という感じでややルーズな形でテストを書いています。
この部分はまだまだアップデートの余地があるな、と考えているところです。
まとめ
本日はテストにおけるデトロイト派とロンドン派についてと自分の考えを書いてみました。
ロンドン派とデトロイト派は対立する思想ではなく「テスト観点が異なるもの」に過ぎません。
各プロダクトの特性を踏まえてどちらの戦略をどの程度まで取り込むかを考えることが大事です。
さあ、あなたのプロダクトのソースコードを思い浮かべてみてください。
どの部分にどちらの考え方が使えそうでしょうか?
今日からあなたのプロダクトのテストコードをアップデートしてみませんか。
参考
Views: 0