テストダブルを理解しよう
はじめに
開発を行っている中で、テストダブルに関する言葉の理解が曖昧で、モックやスタブという言葉を適切に使えていないと感じたため、ブログの記事にまとめることにしました。
調べていく中で、Gerard Meszaros 氏著の「xUnit Test Patterns」で Test Double という総称や Dummy / Fake / Stub / Spy / Mock といった分類が体系化されていることを知りました。
本ブログでは、「xUnit Test Patterns」で説明されているテストダブルを簡単に紹介した後に、普段利用している RSpec ではそれらの概念がどのように扱われているのかをまとめます。
xUnit Test Patterns におけるテストダブルの定義
原著は未読のため、以下の記事を参考にテストダブルの分類を紹介します。
テストダブルとは、テスト環境においてテストが依存する本物のオブジェクトを置き換える役割を持つものの総称です。
テストダブルは役割ごとに大きく5つに分類されます。
- テストダブル
- ダミー
- フェイク
- スタブ
- スパイ
- モック
各テストダブルの定義については、Martin Fowler 氏の記事「Test Double」の説明を日本語に訳して紹介します。
- ダミー(Dummy) : ダミーオブジェクトは渡されるものの、実際には使用されることはありません。通常、パラメータリストを埋めるためにのみ使用されます。
- フェイク(Fake) : フェイクオブジェクトは実際に動作する実装を持っていますが、通常は何らかの近道をとっているため、本番環境には適していません(InMemoryTestDatabase が良い例です)。
- スタブ(Stub) : スタブは、テスト中に呼び出された際にあらかじめ用意された応答を返すもので、通常、テスト用にプログラムされた内容以外のものには一切応答しません。
- スパイ(Spy) : スパイは、呼び出し方法に基づいて情報を記録する機能を持つスタブです。その一例として、送信されたメッセージ数を記録するメールサービスなどが挙げられます。
- モック(Mock) : モックは、受け取るべき呼び出しの仕様を定義する「期待値」が事前にプログラムされています。予期しない呼び出しを受けた場合は例外をスローすることができ、検証の段階で、予期していたすべての呼び出しが確実に受け取られたかどうかがチェックされます。
今回取り上げた xUnit Test Patterns における分類が唯一の標準というわけではありません。
例えば、「Software Engineering at Google」でもテストダブルという総称は同様に使われていますが、その使い方を Faking / Stubbing / Interaction Testing という3つのテクニックに分類しており、用語の整理方法が異なります。
ただし、当時スタブやモックなどの用語が人によって異なる意味で使用されていた中で、Gerard Meszaros氏 は「Test Double」という総称を導入し、それらの概念を整理しました。
その分類は後に xUnit Test Patterns で体系的にまとめられ、Dummy / Fake / Stub / Spy / Mock という分類として広く知られるようになりました。
そのため、これらを理解しておくことはソフトウェア開発におけるテストを学ぶ上で重要だと考えています。
RSpec におけるテストダブル
では、RSpec ではテストダブルがどのように扱われているのか見ていきます。
RSpec においても、テストダブルは「テスト中に本物のオブジェクトの代わりを務めるオブジェクト」という意味で使われています。
RSpec では、基本的に汎用的なテストダブルを作成し、それに対して Method Stub や Message Expectation などを設定する API が提供されています。
ここからは、RSpecで特によく利用するスタブ・スパイ・モックについて見ていきます。
以下で紹介するコードは一例であり、RSpec にはそれぞれの役割を実現するための他の API も用意されています。
スタブ(Stub)
RSpec の Method Stub は、オブジェクトに対して「特定のメッセージを受け取ったら、あらかじめ決められた値を返す」という振る舞いを設定するものです。
実際のコードのイメージは下記です。
RSpec.describe 'stub test' do
# 処理の流れ
# 1. SmsSender の代役(instance_double)を用意する
# 2. Stub で「send されたら accepted を返す」と決め打ちする
# 3. NotifyService を実行する(内部で send → accepted なら送信履歴を作成)
# 4. 送信履歴レコードが作成されたことを検証する
it 'creates a send history when sms api returns accepted' do
sms_sender = instance_double(SmsSender)
# Stub: 外部 API の応答を決め打ちする
allow(sms_sender).to receive(:send).and_return('accepted')
NotifyService.new(sms_sender).call
expect(SendHistory.exists?).to be true
end
end
テストが実行されるたびに実際の SMS を送信すると費用がかかってしまうため、SMS API が 'accepted' を返したものとしてスタブします。
今回のケースでは、SMS API が 'accepted' を返した場合に、送信履歴が正しく作成されることをテストしています。
スパイ(Spy)
呼び出しを記録できる状態にしておき、テスト対象の実行後に「どう呼ばれたか」を have_received で検証します。
実際のコードのイメージは下記です。
RSpec.describe 'spy test' do
# 処理の流れ
# 1. SmsSender の代役(instance_double)を用意する
# 2. allow で send の呼び出しを記録できるようにする(Spy 化)
# 3. NotifyService を実行する(内部で sms_sender.send が呼ばれる)
# 4. have_received で「正しいメッセージで send されたか」を検証する
it 'sends sms with the expected message' do
sms_sender = instance_double(SmsSender)
# Spy: 呼び出しを記録できるようにする
allow(sms_sender).to receive(:send)
NotifyService.new(sms_sender).call
expect(sms_sender).to have_received(:send).with('hello')
end
end
スタブでは、依存オブジェクトから返される値を固定することで、テスト対象の状態や返り値を検証しやすくしています。一方、スパイでは依存オブジェクトがどのように呼び出されたかを検証します。
モック(Mock)
あらかじめ「どのような呼び出し(メソッドや引数)を受けるべきか」という期待を設定し、example 終了時にその期待が満たされたかを検証します。
RSpec では expect(...).to receive がこれに対応します。
実際のコードのイメージは下記です。
RSpec.describe 'mock test' do
# 処理の流れ
# 1. SmsSender の代役(instance_double)を用意する
# 2. Mock で「send が hello という引数で呼ばれるはず」と事前に宣言する
# 3. NotifyService を実行する(内部で sms_sender.send が呼ばれる)
# 4. example 終了時、期待どおり呼ばれていれば成功 / 違えば失敗
it 'sends sms with the expected message' do
sms_sender = instance_double(SmsSender)
# Mock: 実行前に呼び出しの期待を設定する
expect(sms_sender).to receive(:send).with('hello')
NotifyService.new(sms_sender).call
end
end
スパイと同様に「正しい呼び出しになっているか」を見ますが、検証のタイミングが違います。
スパイ : 呼び出しを記録しておき、実行後に have_received で検証する
モック : 実行前に expect(...).to receive で期待する呼び出しを設定する
まとめ
本記事を通して、xUnit Test Patterns におけるテストダブルの定義と、それぞれの役割について整理することができました。
xUnit Test Patterns では、Dummy / Fake / Stub / Spy / Mock をテストダブルの役割として分類しています。一方、RSpec の API はこれらの分類と1対1に対応しているわけではなく、テストダブルに対してどのような設定や検証を行うかによって、Stub / Spy / Mock に相当する役割を実現できます。
これまで何となく使っていたモックやスタブといった言葉について、元となる概念を理解することで、テストコードの意図や目的をより明確に捉えられるようになりました。