Mock と Stub について
初めまして、リコーの沖田です。この度私もこの blog を書くことになりました。以後よろしくお願いいたします。
みなさんテストは好きですか?私も含めて私の同僚は皆テストが大好きなので、しばしばテストの議論で白熱しすぎてしまいます。今日はそのテストの中から Mock(モック) と Stub(スタブ) について書いてみたいと思います。
Test Double
まずテストにおける Mock と Stub についてですが、これらは Test Double という概念の一部です。Double とは代役という意味で、テスト対象となるシステムが依存する外部のコンポーネントの代わりに、それらしく振舞ってくれるコンポーネントを代役として利用しようということです。
例えば Web アプリの Controller の単体テストがしたい場合に、Model の実装が完了するまでテストができないっていうのでは大変ですよね。そこで、Model の Mock や Stub を利用することにより、Model の影響を受けることなく、Controller の単体テストだけに専念することができます。
このように Test Double を利用することで、依存を排除し、テストを効率良く作成・実行することができます。
Test Double についての詳細は「xUnit Test Patterns(Gerard Meszaros, 2007)」という本や
http://xunitpatterns.com/Test%20Double.html
に書いてあります。なお、Mock と Stub 以外にも Spy, Fake, Dummy 等の Test Double もあるとのことです。
Mock と Stub の違い
さて、では Mock と Stub の違いはなんでしょうか。
先ほどの Test Double の定義では、Mock はテスト時の呼び出しの仕様をあらわしたもので、Stub はテスト時の呼び出しに対して決められた値を返すものです。
これを単に Mock は呼び出しの verification をして、Stub はそれをしないと認識してしまうと、どのようにして Mock と Stub を使い分けるかわからなくなってしまいます。さらにいえば、Mock と Stub が区別されている理由さえわからなくなります。
Mock と Stub の違いはテストの観点の違いです。相互作用(振る舞い)中心のテストに利用するのがMockで、状態中心のテストに利用するのがStubです。
相互作用中心のテストとはテスト対象のシステムと外部のコンポーネントとの間で正しいやり取りがされるかのテスト、いわばプロトコルのテストです。外部のコンポーネントは Mock により置き換えられ、システムから正しい呼び出しがなされているかを監視します。
したがって、例えばWebアプリの Controller の単体テストにおいて相互作用中心のテストが正しく行われてパスしているならば、Model が正しく実装された時に Controller が正しく動作するということが、Model が実装されなくても保障されます。
一方状態中心のテストとはテスト対象のシステムが正しい結果を返すかというテストです。したがって、最終的に返ってくる結果だけが重要なのですが、その過程において外部のシステムとの統合が面倒な時に Stub を利用してテストを簡単にします。なので、 Stub は信頼できる外部のコンポーネント(例えばDBとかLDAPとか)に対して適応するといいと思います。
相互作用中心のテストと状態中心のテスト
さて相互作用中心のテストと状態中心のテスト2種類の観点でのテストが出てきましたが、どちらのテストをやればよいのでしょうか? 結論からいうと両方やるべきです。
例えば BDD(Behavior Driven Development) では、その名前から振る舞いすなわち相互作用中心のテストだけをやればいいと錯覚してしまいます。たしかに Mock を利用して振る舞いの仕様を作成するのが BDD での spec 作成の大半を占めます。しかし、仕様書の観点からすると、振る舞いすなわちアルゴリズムだけの記述だけでは不十分で、入出力の例が書いてある必要があります。その入出力例を記述するのが状態中心のテストなので、両方やるべきなのです。
もちろん、コンポーネント毎に相互作用中心のテストと状態中心のテストのどちらに比重をおくべきかを考えることが重要になるわけですが。
RR(Double Ruby)
さて Mock を使った RSpec のテストを実際に書いてみようと思います。RSpec には標準で Mock が用意されているのですが、今回は RR(Double Ruby) というライブラリを利用してSinatraで作る blog のテストを書いてみようと思います。(記事の都合上基本的な Sinatra, RSpec, DataMapper の説明は省略しています)
まず RSpec の Mock を RR の Mock で置き換える設定をします。これで rspec の Mock が RRの Mock に置き換えられます。また、Sinatra のテストをするため、Rack::Test::Methods を include しています。
Spec::Runner.configure do |config|
config.include Rack::Test::Methods
config.mock_with :rr
end
次に、’/posts/1′ にアクセスすると、DataMapper により ID が 1 の記事が読み込まれ、その article が表示されるという仕様を書いてみます。
describe '/posts/:id' do
it 'should load Posts with id' do
mock(Post).get!('1') { mock(Post.new).article { 'article of 1' } }
get('/posts/1')
last_response.body.should == 'article of 1'
end
end
Post は DataMapper で作成した Model です。その Post の Mock に対して get! が呼ばれることを verification しています。中括弧の中は article が呼ばれた時の返り値を指定しています。では、このテストが落ちることを確認したら実装をします。
def '/posts/:id' do
post = Post.get!(params[:id])
post.article
end
実装後テストをしてみると、実際にDBに値を入れなくても(DBの状態にかかわらず)テストが通ります。前述したように外部への依存がなく効率の良いテストができる ことが実感できます。
ちなみに RSpec の Mock を利用する場合はこのようにかけます。
Post.should_receive(:get!).with('1')\
.and_return mock(:article => 'article of 1')
RR の方が実際のコードに近く、かつ短く書けるので素敵です。さらにいえば mock(Post.new) の部分を Machinist を利用して
Post.make_unsaved(:article => 'article of 1')
と書くとさらに素敵なのですが、Machinist については次回以降ということにします。
また、私の同僚が RR は RSpec に比べて matcher の機能が足りない部分があるので、既存の rspec での Mock を RR に置き換える時に困ることがあると言っていました。そこで RSpec と RR(0.10.2) の matcher を見比べたところ例えば hash_including という matcher で差異が見られました。
RSpec の matcher では hash_including は
Foo.should_receive(:color).with(:red, :blue => false)
のように Hash の key の存在確認と key value の組み合わせの両方が指定できます。
一方、RR では
mock(Foo).color(:red => true, :blue => false)
のように key value の組み合わせでしか指定できません。
これの解決には例えば satisfy という matcher を利用するか、RR::WildcardMatchers::HashIncluding.wildcard_match? を書き換えるかすればいいと思います。ここでは、satisfy を利用する例を紹介しておこうと思います。
def hash_has_keys(*args)
satisfy do |hash|
args.all? do |arg|
arg.is_a?(Hash) ? arg.all? {|k, v| hash[k] == v } : hash.has_key?(arg)
end
end
end
これを以下のような感じで hash_including という matcherの代わりに利用すれば、
mock(Foo).color(hash_has_keys(:red, {:blue => false, :yellow => false}))
RSpec の hash_including と同様に使えます。
ただし、こうすると matcher が失敗した場合に、
unexpected method invocation:
color({:red=>true, :green=>false, :blue=>false})
expected invocations:
- color(satisfy {block})
と表示されてエラーメッセージがいまいちわかりにくくなってしまうので注意してください。
長くなりましたので、最後にまとめますと、本エントリでは
- Test Doubleの定義に基づく Mock と Stub の違い
- Mock を利用する相互作用中心のテストと Stub を利用することができる状態中心のテストの使い分け
- 相互作用中心のテストを RR の Mock を使って書く例および RR の tips
について説明しました。
