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.
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.
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
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.
If you see a position that suits, why not apply today?
Find out moreWe create this content for general information purposes and it should not be taken as advice. Always take professional advice. Read our full disclaimer
Keep up to date with Simply Business. Subscribe to our monthly newsletter and follow us on social media.
Subscribe to our newsletter6th Floor99 Gresham StreetLondonEC2V 7NG
Sol House29 St Katherine's StreetNorthamptonNN1 2QZ
© Copyright 2023 Simply Business. All Rights Reserved. Simply Business is a trading name of Xbridge Limited which is authorised and regulated by the Financial Conduct Authority (Financial Services Registration No: 313348). Xbridge Limited (No: 3967717) has its registered office at 6th Floor, 99 Gresham Street, London, EC2V 7NG.