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
endAPIドキュメントの説明を見る
そもそも #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にしておけばエラーが起こるので安心
- この場合はどちらも
その他参考資料

h3pei
フリーランスのソフトウェアエンジニア。Ruby / Rails アプリケーションの開発が得意領域。設計・実装・運用まで含めてプロダクト開発が好きです。
MENTAでアプリケーション開発や学習の支援・伴走もしています。