Androidgrl's Blog About 3D Printing & Tech, by Jamie Kawahara

The difference between Rspec spies, doubles and instance_doubles

Throughout the code base of our Rails app at work, we have many Rspec tests that use spies, doubles and instance_doubles. I found most of the information on the web about them to be very theoretical and not practical. My coworker then explained it to me very clearly as follows:

  1. All three terms belong under an umbrella term called “doubles”. In other words, a spy is a type of double, and so is an instance_double.

  2. Spies, doubles, and instance_doubles are doubles that increase in their power of specificity in that order. So a double is more specific than a spy and an instance_double is more specific than a double.

The best way to demonstrate how they work is to pry into an rspec file. This assumes that you are already familiar with setting up an Rspec test file in a Rails app. In the Rspec file, inside an “it” block place a binding.pry.

    it "demonstrates doubles" do
      binding.pry
    end

In the pry session, define a spy, then try to call undefined methods on it. It will keep returning a double. So spies, are just generic doubles that will keep giving you a double no matter what methods you call on them.

pry(#<RSpec>)> s=spy
=> #<Double (anonymous)>
pry(#<RSpec>)> s.what?
=> #<Double (anonymous)>
pry(#<RSpec>)> s.anything?
=> #<Double (anonymous)>

Moving towards more specificity, define a double. Below a double is defined with a method called name that returns the string “androidgrl”. When we call name on the double it returns “androidgrl”. What happens when we call an undefined method like “age” on the double? It gives us an error unlike with spies. So with doubles, you have to define methods that can be called on them.

pry(#<RSpec>)> d=double(name: "androidgrl")
=> #<Double (anonymous)>
pry(#<RSpec>)> d.name
=> "androidgrl"
pry(#<RSpec>)> d.age
RSpec::Mocks::MockExpectationError: #<Double (anonymous)> received unexpected message :age with (no args)
from /opt/active/acl_api/current/vendor/bundle/ruby/2.1.0/gems/rspec-support-3.3.0/lib/rspec/support.rb:86:in `block in <module:Support>'

And finally, we have the most specific type of double, instance_doubles. Here we have to specify which Class we’re defining the double for. This has to be an actual class that exists either in your application or inherited through Ruby. Here we’re stubbing out the “count” method on the Array class to always return 3. Notice that when we try to define a method on Array that doesn’t actually exist we get an error. Also when we try to make an instance double for a Class that doesn’t exist we get an error.

pry(#<RSpec>)> i=instance_double(Array, count: 3)
=> #<InstanceDouble(Array) (anonymous)>
pry(#<RSpec>)> i.count
=> 3
pry(#<RSpec>)> i=instance_double(Array, whatever: "?")
RSpec::Mocks::MockExpectationError: the Array class does not implement the instance method: whatever
from /opt/active/acl_api/current/vendor/bundle/ruby/2.1.0/gems/rspec-support-3.3.0/lib/rspec/support.rb:86:in `block in <module:Support>'
pry(#<RSpeci=instance_double(FakeClass, fake_method: "fake")
NameError: uninitialized constant TM::FakeClass
from (pry):14:in `block (2 levels) in <module:TM>'

In summary:

  1. Instance_doubles have to be defined on Classes that actually exist in your code and you can only stub methods that actually exist.

  2. Doubles can have any methods called on them as long as you define them in the double.

  3. Spies don’t need to have anything defined on them and will always return a double.