instance_doubleとinstance_spyの使い分け
RSpecの #instance_double
と #instance_spy
の使い分けについて考察してみた。個人の見解です。
環境・バージョン
- ruby 2.7.0p0
- rspec-mocks 3.9.0
#instance_double
と #instance_spy
の違い・共通点
Verifying Doubles
#instance_double
も #instance_spy
も、Verifying Double と呼ばれるオブジェクトを返すという共通点がある。
これは何かと言うと、テストダブル対象のインスタンスに存在しないメソッドがスタブされた場合や、引数が異なる形式でメソッドが呼び出された場合にエラーを起こしてくれるという機能を持つオブジェクトである。
一方、#double
や #spy
が返すオブジェクトにはそういった機能はなく、存在しないメソッドをコールしても良いし、実際の異なる引数形式でメソッドを呼び出しても良い「何でもあり」な状態になっている。
以下は #double
と #instance_double
でこの挙動を比較したサンプルコード。
# foo.rb
class Foo
def run
'foo'
end
end
# spec/foo_spec.rb
RSpec.describe do
describe '#run' do
context 'when using #instance_double' do
it "doesn't raise error" do
foo = instance_double(Foo)
allow(foo).to receive(:run)
allow(Foo).to receive(:new).and_return(foo)
# => Foo#runは存在するためエラーは起きない
end
it "raises error" do
foo = instance_double(Foo)
allow(foo).to receive(:hoge)
allow(Foo).to receive(:new).and_return(foo)
# => Foo#hogeは存在しないためエラー
# `the Foo class does not implement the instance method: hoge`
end
end
context 'when using #double' do
it "doesn't raise error" do
foo = double(Foo)
allow(foo).to receive(:hoge)
allow(Foo).to receive(:new).and_return(foo)
# => Foo#hogeは存在しないがエラーは起きない
end
end
end
end
詳しくは以下を参照。
https://relishapp.com/rspec/rspec-mocks/v/3-9/docs/verifying-doubles
as_null_object
実装レベルで見ると1点だけ違いがあり、#instance_spy
は #instance_double
の返り値に対して #as_null_object
メソッドを呼び出している。
# lib/rspec/mocks/example_methods.rb
def instance_spy(*args)
instance_double(*args).as_null_object
end
#as_null_object
の機能はAPIドキュメントの通り。
Tells the object to respond to all messages. If specific stub values are declared, they'll work as expected. If not, the receiver is returned.
つまり、「全てのメソッド呼び出しに応答するようになり、返り値は指定すればそれを返すが何も指定しない場合はレシーバ自身を返す」という機能が追加される。
「全て」とはあるが、前述のVerifying Doubleにおいてはテストダブル対象のインスタンスに存在するメソッドだけが対象となる。
以下は #spy
と #instance_spy
でこの挙動を比較したサンプルコード。
# foo.rb
class Foo
def run
'foo'
end
end
# spec/foo_spec.rb
RSpec.describe do
describe '#run' do
context 'when using #instance_spy' do
it "raises error" do
foo = instance_spy(Foo)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.hoge
# => Foo#hogeは存在しないためエラー
# `the Foo class does not implement the instance method: hoge`
end
end
context 'when using #spy' do
it "doesn't raise error" do
foo = spy(Foo)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.hoge
# => Foo#hogeは存在しないがエラーは起きない
end
end
end
end
存在するメソッドを自動でスタブするか否か
上記2点の共通点と違いからもわかることではあるが、#instance_double
は呼び出すメソッドを明示的に allow..
で定義する必要があるが、 #instance_spy
はこれを明示的に行う必要が無い。
#as_null_object
により、#instance_spy
はテストダブル対象のクラスに存在するメソッドについては自動で応答するようにしてくれる。
# foo.rb
class Foo
def run
'foo'
end
end
# spec/foo_spec.rb
RSpec.describe do
describe '#run' do
context 'when using #instance_double' do
it "raises error" do
foo = instance_double(Foo)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.run
# => `#run` を呼べるようにallowで定義していないのでエラー
# #<InstanceDouble(Foo) (anonymous)> received unexpected message :run with (no args)
end
it "doesn't raise error" do
foo = instance_double(Foo)
allow(foo).to receive(:run)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.run
# => allowで定義したのでエラーは起きない
end
end
context 'when using #instance_spy' do
it "doesn't raise error" do
foo = instance_spy(Foo)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.run
# => エラーは起きない
end
end
end
end
APIドキュメントの説明を見る
そもそも #instance_spy
のAPIドキュメントには以下のような説明がある。
Constructs a test double that is optimized for use with have_received against a specific class.
#have_received
マッチャでメソッドが呼び出されたかどうかを確認するために最適化されているとのこと。
とはいえ、前述のとおり #instance_double
でも allow..
でメソッドをスタブすれば、同じように #have_received
で確認できるので、最適化されているというのが具体的に何を指し示しているのかは読み取れなかった。
# foo.rb
class Foo
def run
'foo'
end
end
# spec/foo_spec.rb
RSpec.describe do
describe '#run' do
context 'when using #instance_double' do
it "doesn't raise error" do
foo = instance_double(Foo)
allow(foo).to receive(:run)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.run
expect(foo).to have_received(:run) # => pass
end
end
context 'when using #instance_spy' do
it "doesn't raise error" do
foo = instance_spy(Foo)
allow(Foo).to receive(:new).and_return(foo)
Foo.new.run
expect(foo).to have_received(:run) # => pass
end
end
end
end
使い分けの方針
以上のような共通点・違いを理解した上で、次のような使い分けをすると良いんじゃないかと考えた。
- メソッドが呼び出されたかどうかのみに関心があり、返り値や呼び出し時の引数に関心が無い場合は
#instance_spy
を使う- この場合、単純に
#instance_double
よりもシンプルに書けるため。
- この場合、単純に
- 呼び出すメソッドの返り値や呼び出し時の引数に関心がある場合は
#instance_double
を使う- この場合はどちらも
allow..
で返り値を明示的に定義する必要はあるが、#instance_spy
を使う積極的な理由が無い。 #instance_spy
だとデフォルトでレシーバ自身を返すようになっているが、返り値に関心がある場合はそれにメリットはない。下手に定義忘れをしていても動いてしまうくらいならば、#instance_double
にしておけばエラーが起こるので安心
- この場合はどちらも