[RSpec]Mock/Stub/Null Object/Spy
こんにちは。kubopです。
最近、Effective Testing with RSpec 3: Build Ruby Apps with Confidence (English Edition)という本を読んでいて学んだことがあり、一部をメモ的に記します。(英文なので意訳あり)
Understanding Test Doubles
RSpec のようなテストフレームワークではダブルが、テストダブルが映画などで「スタンドダブル」と言われるようなシステムの一部を切り離して振舞う役割を担う。
Types of Test Doubles
スタブ(Stubs)
意味のある計算やI/Oを避け、定型応答を返す。モック(Mocks)
特定のメッセージを期待する。例題の終わりまでに受け取らなければエラーを発生させる。Nullオブジェクト (Null Object)
任意のオブジェクトの代用となり、任意のメッセージに応答して自身を返す。スパイ(Spy)
受け取ったメッセージを記録し、後で確認できるようにする。
※ ここでは、Gerard Meszaros が開発した語彙をベースにしています。
利用方法に加えて、テストダブルにはオリジンがあり、その基礎となるRubyクラスが何であるかを示す。(本物のRubyオブジェクトもあれば、全くのフェイクもある!
Pure Double
通常のモックオブジェクト。Partial Double
既存の Ruby オブジェクトにテスト用のダブルの振る舞いをさせたもの。Verifying Double
Pure Doubleのように完全に偽物ですが、Partial Doubleのように実際のオブジェクトに基づいてそのインターフェイスを制限されている。Stubbed Constant
クラス名やモジュール名などの Ruby の定数で、ひとつのテストのために作成、削除、置換を行う。
Usage Modes: Mocks, Stubs, and Spies
# 準備/スタンドアロンで実行する。
pry(main)> require 'rspec/mocks/standalone'
=> true
Generic Test Doubles
RSpec のdoubleメソッドは、どのモードでも使用できる汎用的なテスト用の double を作成。
pry(main)> ledger = double
=> #<Double (anonymous)>
このダブルは、普通のRubyのオブジェクトと同じように振る舞います。
メッセージは指定されたもののみ受け付け可能。
pry(main)> ledger.record(an: :expense)
RSpec::Mocks::MockExpectationError: #<Double (anonymous)> received unexpected message :record with ({:an=>:expense})
from /usr/local/bundle/gems/rspec-support-3.11.0/lib/rspec/support.rb:102:in `block in <module:Support>'
Stubs
あらかじめプログラムされた、定型的なレスポンスを返す。
スタブは、値を返すが副作用を実行しないメソッドをシミュレートするときに最適。
pry(main)> http_response = double('HTTPResponse', status: 200, body: 'OK')
=> #<Double "HTTPResponse">
pry(main)> http_response.status
=> 200
pry(main)> http_response.body
=> "OK"
-- もちろんメッセージの設定は別でもOK --
pry(main)> http_response = double('HTTPResponse')
=> #<Double "HTTPResponse">
pry(main)> allow(http_response).to receive_messages(status: 200, body: 'OK')
=> {:status=>200, :body=>"OK"}
Mocks
モックは、コマンドメソッドを扱うときに便利。
システムからイベントを受け取る
そのイベントに基づいて判断する
副作用のあるアクションを実行する
例えば、チャットボットの返信機能。
テキストメッセージを受信し、どのように返信するかを決定し、チャットルームにメッセージを投稿することができる。
この動作をテストするためには、決まった戻り値を提供するだけでは不十分。
投稿するという副作用を正しくトリガーすることを確認する必要がある。
モックオブジェクトを使うには、そのオブジェクトが受け取るべきメッセージのセット、メッセージ期待値をあらかじめ用意しておく必要がある。
宣言は通常と同じように、expectメソッドとマッチャーを組み合わせて行う。
-- のちほど --
Null Object
これまで定義してきたダブルは厳密なもので、どのようなメッセージが許されるかをあらかじめ宣言する必要があります。
しかし、テストダブルが複数のメッセージを受け取る必要がある場合は、 その都度宣言しなければならず、テストがもろくなる可能性がある。
pry(main)> null_object = double('NullObject').as_null_object
=> #<Double "NullObject">
pry(main)> null_object.fly
=> #<Double "NullObject">
このタイプのNullオブジェクトはブラックホールと呼ばれ、送られたメッセージに応答し、常に自分自身を返す。
つまり、次から次へとメソッドコールを連鎖させることができ、好きなだけ呼び出すことができる。
Spy
Spiesは従来のフローを復元する1つの方法。
以下のクラスのcharacter.jumpが呼ばれているか調べたい場合。
class Game
def self.play(character)
character.jump
end
end
pry(main)> character = instance_spy('Character')
=> #<InstanceDouble(Character) (anonymous)>
pry(main)> Game.play(character)
=> #<InstanceDouble(Character) (anonymous)>
pry(main)> expect(character).to have_received(:jump)
=> nil
double の場合は呼び出されるメソッドすべてを明示的にスタブする必要がありましたが、spy の場合はその必要がない。
というのも、instance_spyはas_null_objectのエイリアスのようです。(RSpec3.1から利用可能)
Origins: Pure, Partial, and Verifying Doubles
Pure Doubles
これまでに書いたダブルは、全てPure Doublesである。
これらはrspec-mocksによって専用に作成されて、指定された動作のみで構成されている。
Partial Doubles
Pure Doublesでは簡単に使用出来、依存関係のあるコードをテストするのに適しているが、現実では一部をオーバーライドしてすり替えるという方法が最適だったりする。
例えば、Time.nowやRandom.randは、それをオーバーライドする方法がない。
その場合、部分的にメソッドをオーバーライドするように見せかけてダブルを用意することが出来る。
pry(main)> random = Random.new
=> #<Random:0x000055f047ba4608>
pry(main)> allow(random).to receive(:rand).and_return(0.1234)
=> #<RSpec::Mocks::MessageExpectation #<Random:0x000055f047ba4608>.rand(any arguments)>
pry(main)> random.rand
=> 0.1234
Verifying Doubles
上記のような例では、テストダブルと、本番コードの依存関係がずれてしまうことがある。例えば、Randomのメソッドが変更されたら、削除されたら…。
この場合は、Verifying Doublesを利用し、インターフェースを制限する。Verifying Doublesを利用すると、元クラスのメソッドに変更があった場合は即座にテストが失敗する。
instance_double('SomeClass')
SomeClass のインスタンスメソッドを使用して double のインターフェイスを制限する。class_double('SomeClass')
SomeClass のクラスメソッドを使用して double のインターフェイスを制限する。object_double(some_object)
クラスではなく、some_object のメソッドを使用して double のインターフェイスを制限する。(※ method_missing を使用する動的なオブジェクトに便利。
Stubbed Constants
スタブ定数を使用すると、1つの例の間、定数を別のものに置き換えることができる。
class Hoge
CONST_EXAMPLE = 10
...
end
stub_const('Hoge::CONST_EXAMPLE', 1)
stub_const を使って、いろいろなことができます。
新しい定数を定義する
既存の定数を置き換える
モジュールやクラス全体を置き換える (これらも定数であるため)
重いクラスの読み込みを避け、代わりに軽量な偽物を使用する
hide_const('ActiveRecord')
上記のように、利用させたくないモジュールなどを制限させることも可能。