RSpecモック
rspec-mocksは、rspec用のテストダブルフレームワークであり、メソッドスタブ、フェイク、テストダブルや実オブジェクトに対するメッセージの期待をサポートしています。
インストール
gem install rspec # rspec-core、rspec-expectations、rspec-mocksのため gem install rspec-mocks # rspec-mocksのみのため
main
ブランチで実行したいですか? 依存するRSpecリポジトリも含める必要があります。以下をGemfile
に追加してください:
%w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib|
gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'main'
end
テストダブル
テストダブルは、コード例中で他のオブジェクトの代わりに立つオブジェクトです。オプションの識別子を渡してdouble
メソッドを使用して、テストダブルを作成します:
book = double("book")
ほとんどの場合、テストダブルがシステム内の既存のオブジェクトに似ていることを確信したいと思うでしょう。この目的のために、検証ダブルが提供されています。既存のオブジェクトが利用可能な場合、検証ダブルは存在しないメソッドや無効なパラメータ数を持つメソッドに対してスタブや期待を追加することを防ぎます。
book = instance_double("Book", :pages => 250)
検証ダブルには、依存関係をロードせずにテストを独立して実行することと、それらを実際のオブジェクトに対して検証することの両方を可能にするいくつかの巧妙なトリックがあります。詳細は、彼らのドキュメントで確認できます。
検証ダブルは、double()と同様にカスタム識別子も受け入れることができます。例:
books = []
books << instance_double("Book", :rspec_book, :pages => 250)
books << instance_double("Book", "(Untitled)", :pages => 5000)
puts books.inspect # with names, it's clearer which were actually added
メソッドスタブ
メソッドスタブは、事前に決められた値を返す実装です。メソッドスタブは、同じ構文を使用してテストダブルまたは実オブジェクトに宣言することができます。rspec-mocksは、メソッドスタブを宣言するため の3つの形式をサポートしています:
allow(book).to receive(:title) { "The RSpec Book" }
allow(book).to receive(:title).and_return("The RSpec Book")
allow(book).to receive_messages(
:title => "The RSpec Book",
:subtitle => "Behaviour-Driven Development with RSpec, Cucumber, and Friends")
また、次のショートカットも使用できます。これにより、テストダブルが作成され、メソッドスタブが1つの文で宣言されます:
book = double("book", :title => "The RSpec Book")
最初の引数は名前であり、ドキュメントに使用され、失敗メッセージに表示されます。名前に関心がない場合は省略しても構いません。これにより、組み合わせたインスタンス化/スタブ宣言が非常に簡潔になります。
double(:foo => 'bar')
これは、それらを反復処理するメソッドにテストダブルのリストを提供する場合に特に便利です。
order.calculate_total_price(double(:price => 1.99), double(:price => 2.99))
メソッドチェーンのスタブ化
receive
の代わりにreceive_message_chain
を使用して、メッセージのチェーンをスタブ化することができます。
allow(double).to receive_message_chain("foo.bar") { :baz }
allow(double).to receive_message_chain(:foo, :bar => :baz)
allow(double).to receive_message_chain(:foo, :bar) { :baz }
# Given any of the above forms:
double.foo.bar # => :baz
チェーンは任意の長さにすることができ、これによりデメテルの法則を違反することが容易になります。そのため、receive_message_chain
の使用はコードの臭いと考えるべきです。すべてのコードの臭いが実際の問題を示すわけではありませんが(フルエントインターフェースを考えてみてください)、receive_message_chain
は依然として脆弱な例を生み出します。たとえば、スペックでallow(foo).to receive_message_chain(:bar, :baz => 37)
と書き、実装がfoo.baz.bar
を呼び出す場合、スタブは機能しません。
連続した返り値
スタブが複 数回呼び出される可能性がある場合、and_return
に追加の引数を指定できます。呼び出しはリストを循環します。最後の値は、その後のすべての呼び出しに対して返されます。
allow(die).to receive(:roll).and_return(1, 2, 3)
die.roll # => 1
die.roll # => 2
die.roll # => 3
die.roll # => 3
die.roll # => 3
単一の呼び出しで配列を返すには、配列を宣言します。
allow(team).to receive(:players).and_return([double(:name => "David")])
メッセージの期待
メッセージの期待は、テストダブルが例の終了前にメッセージを受け取ることを期待するものです。メッセージが受信されると、期待は満たされます。そうでない場合、例は失敗します。
validator = double("validator")
expect(validator).to receive(:validate) { "02134" }
zipcode = Zipcode.new("02134", validator)
zipcode.valid?
テストスパイ
テスト中に、指定されたオブジェクトが期待されたメッセージを受け取ったことを検証します。メッセージを検証するためには、指定されたオブジェクトが明示的にスタブ化されているか、またはヌルオブジェクトダブル(例:double(...).as_null_object
)である必要があります。この目的のために、簡単にヌルオブジェクトダブルを作成するための便利なメソッドが提供されています。
spy("invitation") # => same as `double("invitation").as_null_object`
instance_spy("Invitation") # => same as `instance_double("Invitation").as_null_object`
class_spy("Invitation") # => same as `class_double("Invitation").as_null_object`
object_spy("Invitation") # => same as `object_double("Invitation").as_null_object`
このように受け取ったメッセージを検証することで、テストスパイパターンを実装します。
invitation = spy('invitation')
user.accept_invitation(invitation)
expect(invitation).to have_received(:accept)
# You can also use other common message expectations. For example:
expect(invitation).to have_received(:accept).with(mailer)
expect(invitation).to have_received(:accept).twice
expect(invitation).to_not have_received(:accept).with(mailer)
# One can specify a return value on the spy the same way one would a double.
invitation = spy('invitation', :accept => true)
expect(invitation).to have_received(:accept).with(mailer)
expect(invitation.accept).to eq(true)
注意:have_received(...).with(...)
は、スパイが受け取ったメッセージを記録した後に引数が変更されると正しく動作しません。
例えば、次のような場合は正しく動作しません:
greeter = spy("greeter")
message = "Hello"
greeter.greet_with(message)
message << ", World"
expect(greeter).to have_received(:greet_with).with("Hello")
用語
モックオブジェクトとテストスタブ
モックオブジェクトとテ ストスタブという名前は、特殊なテストダブルを示しています。 つまり、テストスタブはメソッドスタブのみをサポートするテストダブルであり、モックオブジェクトはメッセージの期待値とメソッドスタブをサポートするテストダブルです。
ここでは、重複する用語が多くあり、これらのパターンの多くのバリエーション(フェイク、スパイなど)が存在します。 ほとんどの場合、私たちはメソッドレベルの概念について話しており、これらはメソッドスタブとメッセージの期待値のバリエーションであり、それらを_1つ_の汎用オブジェクトであるテストダブルに適用しています。
テスト固有の拡張
a.k.a. パーシャルダブル、テスト固有の拡張は、テストのコンテキストでテストダブルのような振る舞いを持つシステム内の実オブジェクトの拡張です。 このテクニックはRubyでは非常に一般的です。なぜなら、クラスオブジェクトがメソッドのグローバルな名前空間として機能することがよくあるからです。 例えば、Railsでは次のようにします:
person = double("person")
allow(Person).to receive(:find) { person }
この場合、Personをインストゥルメントして、find
メッセージを受け取るたびに定義したpersonオブジェクトを返すようにします。また、メッセージの期待値も設定できます。これにより、find
が呼び出されない場合には例外が発生します。
person = double("person")
expect(Person).to receive(:find) { person }
RSpecはスタブ化またはモック化しているメソッドを独自のテストダブルのようなメソッドで置き換えます。例の最後に、RSpecはメッセージの期待値を検証し、元のメソッドを復元します。
引数の期待値
expect(double).to receive(:msg).with(*args)
expect(double).to_not receive(:msg).with(*args)
同じメッセージに対して複数の期待値を設定することもできます。
expect(double).to receive(:msg).with("A", 1, 3)
expect(double).to receive(:msg).with("B", 2, 4)
引数のマッチャー
with
に渡された引数は、実際の引数と ===
を使用して比較されます。
引数自体ではなく、引数に関する情報を指定したい場合は、rspec-expectations に付属するマッチャーを使用できます。
これらのマッチャーはすべて文法的に意味があるわけではありません(主に RSpec::Expectations との使用を想定して設計されています)が、
独自のカスタム RSpec::Matchers を作成することもできます。
rspec-mocks は、特定の種類の引数を指定するために使用できるいくつかのキーワードシンボルも追加しています:
expect(double).to receive(:msg).with(no_args)
expect(double).to receive(:msg).with(any_args)
expect(double).to receive(:msg).with(1, any_args) # any args acts like an arg splat and can go anywhere
expect(double).to receive(:msg).with(1, kind_of(Numeric), "b") #2nd argument can be any kind of Numeric
expect(double).to receive(:msg).with(1, boolean(), "b") #2nd argument can be true or false
expect(double).to receive(:msg).with(1, /abc/, "b") #2nd argument can be any String matching the submitted Regexp
expect(double).to receive(:msg).with(1, anything(), "b") #2nd argument can be anything at all
expect(double).to receive(:msg).with(1, duck_type(:abs, :div), "b") #2nd argument can be object that responds to #abs and #div
expect(double).to receive(:msg).with(hash_including(:a => 5)) # first arg is a hash with a: 5 as one of the key-values
expect(double).to receive(:msg).with(array_including(5)) # first arg is an array with 5 as one of the key-values
expect(double).to receive(:msg).with(hash_excluding(:a => 5)) # first arg is a hash without a: 5 as one of the key-values
受信回数
expect(double).to receive(:msg).once
expect(double).to receive(:msg).twice
expect(double).to receive(:msg).exactly(n).time
expect(double).to receive(:msg).exactly(n).times
expect(double).to receive(:msg).at_least(:once)
expect(double).to receive(:msg).at_least(:twice)
expect(double).to receive(:msg).at_least(n).time
expect(double).to receive(:msg).at_least(n).times
expect(double).to receive(:msg).at_most(:once)
expect(double).to receive(:msg).at_most(:twice)
expect(double).to receive(:msg).at_most(n).time
expect(double).to receive(:msg).at_most(n).times
順序
expect(double).to receive(:msg).ordered
expect(double).to receive(:other_msg).ordered
# メッセージが順序通りに受信されない場合、テストは失敗します
同じメッセージを異なる引数で指定することもできます:
expect(double).to receive(:msg).with("A", 1, 3).ordered
expect(double).to receive(:msg).with("B", 2, 4).ordered
応答の設定
メッセージの期待値またはメソッドのスタブを設定する場合、オブジェクトに正確な応答方法を指定できます。
最も一般的な方法は、receive
にブロックを渡すことです:
expect(double).to receive(:msg) { value }
ダブルが msg
メッセージを受信すると、ブロックを評価して結果を返します。
expect(double).to receive(:msg).and_return(value)
expect(double).to receive(:msg).exactly(3).times.and_return(value1, value2, value3)
# returns value1 the first time, value2 the second, etc
expect(double).to receive(:msg).and_raise(error)
# `error` can be an instantiated object (e.g. `StandardError.new(some_arg)`) or a class (e.g. `StandardError`)
# if it is a class, it must be instantiable with no args
expect(double).to receive(:msg).and_throw(:msg)
expect(double).to receive(:msg).and_yield(values, to, yield)
expect(double).to receive(:msg).and_yield(values, to, yield).and_yield(some, other, values, this, time)
# for methods that yield to a block multiple times
これらの応答のいずれもスタブに適用することもできます
allow(double).to receive(:msg).and_return(value)
allow(double).to receive(:msg).and_return(value1, value2, value3)
allow(double).to receive(:msg).and_raise(error)
allow(double).to receive(:msg).and_throw(:msg)
allow(double).to receive(:msg).and_yield(values, to, yield)
allow(double).to receive(:msg).and_yield(values, to, yield).and_yield(some, other, values, this, time)
任意の処理
時折、利用可能な期待値が特定の問題を解決できないことがあります。 特定の長さを持つ配列引数と一緒にメッセージが来ることを期待しているが、中身はどうであれ関係ない場合、次のようにすることができます:
expect(double).to receive(:msg) do |arg|
expect(arg.size).to eq 7
end
スタブされるメソッド自体がブロックを受け取り、特別な方法でそれに yield する必要がある場合は、次のようにします:
expect(double).to receive(:msg) do |&arg|
begin
arg.call
ensure
# cleanup
end
end