Blog icon

How to toe the line with RSpec, Stubs, and RuboCop

By Riana Ferreira,
Riana Ferreira
Scroll down to read

Recently I set up the Rubocop gem for a project. I wanted to use it to ​ensure that my code base aligned with the community Ruby Style Guide. When I ran the tool, I came up against the following error message:

spec/interactors/allow_box_to_be_reused_spec.rb:132:9: C: Avoid stubbing using allow_any_instance_of.
allow_any_instance_of(AllowBoxToBeReused).to receive(:recycle_box).

This message didn't give me any idea of what the problem was, nor what I should do to avoid it in the future.

As there is no generic solution, the code under test will help you to determine what needs to be done. In order to do this you need to:

  • understand how rspec mocking and stubbing works.
  • be clear on what you are testing.
  • clean up your code.

This was the class under test:

class AllowBoxToBeReused
  include Interactor

  delegate :box, to: :context

  def call
    context_failed_message unless recycle_box
  end

  private

  def recycle_box
    box.update(content: nil)
  end

  def context_failed_message
    context.fail!(
      message: I18n.t(".errors.box_can_not_be_reused"),
    )
  end

end

And this was the problematic spec:

context box recycling fails do

  subject(:context) do
    AllowBoxToBeReused.call(box: box)
  end

  let(:box) { FactoryGirl.build(:box, :with_content)

  before do
    allow_any_instance_of(AllowBoxToBeReused).to receive(:recycle_box).and_return(false)
  end

  it "fails to update" do
    expect(context.failure?).to be_truthy
  end

end

What is wrong with this spec?

The example manipulates the internal behaviour of the class to make it respond in a certain way without testing the actual code. In other words, I was making too many assumptions about how the class actually works.

When testing a class, you should test how the code works only by controlling the external dependencies.

In this example, the class has an external dependency on the Box class. When the update method is invoked for an instance of the Box class, it expects a response of:

  • false, if the changes to the object can't be saved
  • true, if the changes to the object are saved

This is what I should have been testing!

A better spec

Here is an example of a much better spec that obeys these principles:

context box recycling fails do

  subject(:context) do
    AllowBoxToBeReused.call(box: box)
  end

  let(:box) { object_double(Box.new, update: false) }

  it "fails to update" do
    expect(context.failure?).to be_truthy
  end

end

Why is this better?

The stubbed external dependency is being passed to the class under test and its actual behaviour is being tested. This allows you to explicitly handle the new behaviour, refactor your code and add more specs to clearly document the new behaviour.

(As a side benefit, sometimes this can uncover unexpected behaviour for the class under test. :-))

You are creating a stubbed class instance object, and ensuring that it actually responds to the method it receives.

Most importantly RuboCop stops complaining!

Finally, for your reading pleasure, here is a handy rspec reference.

To find out how reinteractive can turn your web application vision into reality, get in touch with us through our contact form or call us on +61 2 8019 7252.