Why use doubles in RSpec tests?
If you’re a Ruby programmer or work with Rails, it’s likely you’ll have designed tests using RSpec – the testing library for Rails, to test Ruby code. RSpec features doubles that can be used as ‘stand-ins’ to mock an object that’s being used by another object. Doubles are useful when testing the behaviour and interaction between objects when we don’t want to call the real objects – something that can take time and often has dependencies we’re not concerned with. For example, we may only need to mimic the behaviour of an object in a test. This is where doubles come in useful.
Let’s consider an example:
require 'rspec'
require 'rspec/mocks/standalone'
class Tutor
attr_accessor :name, :subject
def initialize(name, subject)
@name = name
@subject = subject
end
def information
"#{name}" teaches #{subject}."
end
def college
College.find_by_tutor_name(name)
end
end
tutor = double(Tutor, information: 'Jane teaches Ruby')
tutor.information # 'Jane teaches Ruby'
tutor.college #<Double Tutor> received unexpected message :college with (no args)
This test creates a Tutor
double, which only knows about the information
method. If we try to call college
, we get an error that the Tutor
double received an unexpected message. Unlike a real tutor object, we can’t call tutor.college
on the double because it’s not set up to respond to the college
method.
So shouldn’t we just set up the Tutor
double to respond to college
? We could, but we simply don’t need this extra code. In practical terms, to get tutor.college
to return a value, we’d need to have a college table populated with data. But if our test only needs something that looks like a tutor and can access its information
, then using a simple double, as in the above test, will achieve that goal.
Double trouble
But doubles can be controversial. It could be argued that using doubles to abstract away real implementations can cause problems, especially when something has changed in the described class that a double is mocking, e.g. if a method is modified.
In our example RSpec test, there are two classes – Tutor
and Student
. The relationship defines a student as having a tutor. When a new student is created, a tutor is passed in as well as the name. In this test, a double is used to mock out Tutor
when testing the Student
class. The test passes with 0 failures.
require 'rspec'
require 'rspec/mocks/standalone'
class Tutor
attr_accessor :name, :subject
def initialize(name, subject)
@name = name
@subject = subject
end
def information
"#{name}" teaches #{subject}."
end
end
class Student
attr_accessor :name, :tutor
def initialize(name, tutor)
@name = name
@tutor = tutor
end
RSpec.describe Student do
let(:tutor) { double(Tutor, information: 'Jane teaches Ruby.') }
subject { Student.new('James', tutor) }
it { expect(subject.tutor.information).to eq('Jane teaches Ruby.')}
end
# This test passes:
# 1 example, 0 failures
But what happens if we rename the information
method to details
in our code and change what the details
method returns, but forget to update the test?
class Tutor
attr_accessor :name, :subject
def initialize(name, subject)
@name = name
@subject = subject
end
def details
"#{name} teaches #{subject} at ACME College."
end
end
class Student
attr_accessor :name, :tutor
def initialize(name, tutor)
@name = name
@tutor = tutor
end
end
RSpec.describe Student do
let(:tutor) { double(Tutor, information: 'Jane teaches Ruby.') }
subject { Student.new('James', tutor) }
it { expect(subject.tutor.information).to eq('Jane teaches Ruby.')}
end
# This test passes even though #information has changed:
# 1 example, 0 failures
The test passes with 0 failures, but this isn’t accurate because the test no longer represents the Tutor
class correctly. A good test tells us that the Tutor
class has changed. In this example, we want the test to fail so we can fix it and accurately represent the tutor interface.
When to use instance_double
This is where instance_double
can be used to remedy the situation!
In RSpec 3.0 and higher, instance_double
is an implementation of verifying doubles. Verifying doubles are defined in RSpec as “a stricter alternative to normal doubles that provide guarantees about what is being verified. When using verifying doubles, RSpec will check that the methods being stubbed are actually present on the underlying object, if it is available.”
Let’s look at how instance_double
can help in our test:
class Tutor
attr_accessor :name, :subject
def initialize(name, subject)
@name = name
@subject = subject
end
def details
"#{name} teaches #{subject} at ACME College."
end
end
class Student
attr_accessor :name, :tutor
def initialize(name, tutor)
@name = name
@tutor = tutor
end
end
RSpec.describe Student do
let(:tutor) { instance_double(Tutor, information: 'Jane teaches Ruby.') }
subject {Student.new('James', tutor) }
it { expect(subject.tutor.information).to eq('Jane teaches Ruby.')}
end
# Failures
#
# 1) Student
Failure/Error: let(:tutor) { instance_double(Tutor) information: 'Jane teaches Ruby.') }
# # ./double/rb:28:in `block (2 levels) in <top (required)>`
# # ./double/rb:30:in `block (2 levels) in <top (required)>`
# # ./double/rb:31:in `block (2 levels) in <top (required)>`
# Finished in 0.00271 seconds (files took 0.14295 seconds to load)
# 1 example 1 failure
All we’ve done is to use instance-double
instead of double
, replacing double(Tutor, information: 'Jane teaches Ruby.')
with instance_double(Tutor, information: 'Jane teaches Ruby.')
. The test now fails and it forces us to find out whether the information
method is still appropriate to use on the Tutor
double. In this case, it’s not. We need to update the Tutor
double to use the details
method instead.
Here’s the updated test, which now passes.
RSpec.describe Student do
let(:tutor) { instance_double(Tutor, details: 'Jane teaches Ruby at ACME College.') }
subject { Student.new('James', tutor) }
it { expect(subject.tutor.details).to eq('Jane teaches Ruby at ACME College.') }
end
Conclusion
Using instance_double
instead of RSpec’s regular double can ensure the real object and its double stay in sync.
Read more about instance_double
in the RSpec documentation.
See our latest technology team opportunities
If you see a position that suits, why not apply today?
We create this content for general information purposes and it should not be taken as advice. Always take professional advice. Read our full disclaimer
This block is configured using JavaScript. A preview is not available in the editor.