Blog

How to Toe the Line with RSpec, Stubs and RuboCop

Placeholder Avatar
Riana Ferreira
May 10, 2017

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:

```ruby 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:

```ruby 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:

```ruby 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.