Telephone iconCall UsTelephone icon0333 0146 683
Our opening hours
Chevron left icon
Tech blog

Using instance_double in RSpec tests

2-minute read

Cai Gao

29 May 2020

Facebook iconTwitter iconLinkedIn icon

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.

Simply Business technology

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?

Find out more

We create this content for general information purposes and it should not be taken as advice. Always take professional advice. Read our full disclaimer

Find this article useful? Spread the word.

Facebook icon
Share
Twitter icon
Tweet
LinkedIn icon
Post

Keep up to date with Simply Business. Subscribe to our monthly newsletter and follow us on social media.

Subscribe to our newsletter

Insurance

Public liability insuranceBusiness insuranceLandlord insuranceTradesman insuranceProfessionals' insuranceNot for profit insuranceRestaurant insuranceCommercial van insuranceInsurers

Address

6th Floor99 Gresham StreetLondonEC2V 7NG

Sol House29 St Katherine's StreetNorthamptonNN1 2QZ

© Copyright 2020 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.