• Follow us

RSpec Basic: Using Test Doubles

Alberto Alberto Vena #code
16 minutes Read
RSpec Basic: Using Test Doubles

In this blog post we’ll focus on differences between stubs, mocks and spies in RSpec. We think that there’s a lot of wording confusion for those who start using these testing tools and we would like to try to explain how to use them as simply‚Äč as possible.

By creating some example test scenarios we’ll present frequent contexts a developer can come up against while adopting more advanced test techniques. After reading this blog post, readers should have a more clear overview of test doubles and they should be able to understand how and which one of the presented tools to use in order to better solve the situation they are dealing with.

Definition of Test Double

First of all let’s group stubs, mocks and spies into the generic definition of test double.

A test double is a simplified version of an object that allows us to define “fake” methods and their return values.

For example, suppose we have two classes: User and Book. A User can buy multiple books, and the logic is implemented with this code:

class User
  def buy(book, quantity)
    book.decrease_count_on_hand(quantity)
  end
end

class Book
  def decrease_count_on_hand(quantity)
    API::Stock::Book.find(id).decrease_count_on_hand(quantity) # return true or false
  end
end

While testing the buy method on User we probably don’t want to actually call decrease_count_on_hand on Book for several reasons:

  • it could call an external API which is generally slow;
  • it could be hard to configure the database with factories in order to make it work;
  • it is not responsibility of a User unit test to ensure that this method works properly;
  • this method could not have been implemented yet on the API;

Spec without test doubles

How would we test the buy method without using any test double? Probably with something like:

context '#buy' do
  let(:user) { User.create }
  let(:book) { Book.create }

  it 'returns true' do
    expect(user.buy(book, 1)).to eq true
  end
end

Everything is correct here but does it make sense? What are we actually doing here? We are just testing the return value of decrease_count_on_hand method, which is slow and it is probably already tested elsewhere.

We can improve our spec by testing that decrease_count_on_hand on Book is called, which is exactly what the buy method on User is doing.

To accomplish this task we can use test doubles. We are going to analyze three types of usage of test doubles: stubs, mocks and spy.

Stubs

By definition:

A method stub is an implementation that returns a pre-determined value.

This means that we can say RSpec to preventively define the return value of an object method without actually call it.

context '#buy' do
  let(:user) { User.create }
  let(:book) { Book.create }

  it 'returns true' do
    allow(book).to receive(:decrease_count_on_hand).and_return(true)

    expect(user.buy(book, 1)).to eq true
  end
end

This way decrease_count_on_hand is not actually called on Book. Anyway we can do better since we are creating a real Book instance without using it at all. Also, creating a Book instance could trigger API calls or other slow operations.

What if we were able to create a fake object which we could define methods behavior on?

Well, we can by defining a double:

context '#buy' do
  let(:user) { User.create }
  let(:book) { double('fake book') }

  it 'calls decrease_count_on_hand' do
    allow(book).to receive(:decrease_count_on_hand).and_return(true)

    expect(user.buy(book, 1)).to eq true
  end
end

You can imagine a double as a empty object which will only respond to those methods we are going to stub on it.

Mocks

Better, but often not enough: this test makes sense because we are sure that the buy method is calling decrease_count_on_hand. But what would happen if we change the buy method implementation so that in some case it just return true?

def buy(book, quantity)
  return true if book.ebook?
  book.decrease_count_on_hand(quantity)
end

Of course the test will pass. When we need to be sure a method is called we can mock it, which means stub it and set an expectation that it will be called. In this case we want to be sure decrease_count_on_hand is called on the book double only for some scenarios:

context '#buy' do
  let(:user) { User.create }
  let(:book) { double('fake book') }

  context 'when book is digital' do
    before do
      allow(book).to receive(:ebook?).and_return(true)
    end

    it 'does not call decrease_count_on_hand' do
      expect(book).not_to receive(:decrease_count_on_hand)

      user.buy(book, 1)
    end
  end

  context 'when book is not digital' do
    before do
      allow(book).to receive(:ebook?).and_return(false)
    end

    it 'calls decrease_count_on_hand' do
      expect(book).to receive(:decrease_count_on_hand).with(1).and_return(true)

      user.buy(book, 1)
    end
  end
end

This spec breaks if decrease_count_on_hand is not called with 1 as parameter. Also, it comes with a stubbed true return value. From its definition:

A mock is a stub with a built-in expectation to be satisfied during the test.

Note that we had to stub the ebook? method return value since the double wouldn’t have responded to this method otherwise.

Spies

Mocking someway breaks the usual spec flows, that should be similar to the one described by Dan Croak in Four-Phase Test post. We are setting the expectation before executing the code that we want to verify. If we want to respect the “standard” flow we can take advantage of Spies:

Spies are objects that by default can accept all methods without throwing any exception and keep an history of the methods called on them.

We can easily revert the flow:

context '#buy' do
  let(:user) { User.create }
  let(:book) { spy('fake book') }

  it 'calls decrease_count_on_hand' do
    user.buy(book, 1)

    expect(book).to have_received(:decrease_count_on_hand).with(1)
  end
end

More confidence with Verifying Doubles

Working with test doubles it is normal to feel like we are dealing with too much abstract objects, sometime it’s like a complete fiction. Some justified questions that can pop into a developer’s mind could be:

  • what if I delete decrease_count_on_hand method from my Book class?
  • what if the implementation changes and quantity become, for example, the second argument?

All these questions are legitimate, especially because in both cases the answer would be: “yes, tests will yet pass, wrongly. But when more confidence is needed we can use Verifying Doubles, another type of double provided by RSpec. Verifying Doubles are very similar to simple doubles or spies but they also check that methods actually exist on objects and that they are called with parameters consistent with method definition. This way you can even sleep better at night.

context '#buy' do
  let(:user) { User.create }
  let(:book) { instance_spy('Book') }

  it 'calls decrease_count_on_hand' do
    user.buy(book, 1)

    expect(book).to have_received(:decrease_count_on_hand).with(1)
  end
end

For example, above code behaves exactly like the previous example but it also checks that decrease_count_on_hand is defined into Book and it accepts an integer as first parameter.

Verifying doubles are available for class methods as well. You can find more info here

Conclusions: How to choose the right test double?

Here’s some simple rules to keep in mind when we need to choose the right test double in our specs:

  • Choose to stub (using allow) when you want to stub something into the setup phase, which usually is a prerequisite of what you want to test;
  • Choose to mock or spy (using expect) when the stubbed method is also a method you want to test;
  • Choose to use a verifying double when you need to be sure that the method you are stubbing remains consistent with its real implementation.

Join the Conversation