How to Write Future-proof Mocks in RSpec 3

Tests are an important component in most software applications. Whether tests drive the development, or are strapped on after the fact, tests need to be reliable for future development to progress. An application’s complexity rises with time. During and after this increase, a rock solid test suite is crucial.

Let the Dogs Out

Given an application that deals with Dogs:

class Dog
end

Keeping track of grooming statistics about this application’s dogs is the obvious cash cow. But that sounds very tedious and complicated, so we can assume that a GroomingService exists that exposes a single Groomer object:

module GroomingService
  class Groomer
    def initialize(dog)
      @dog = dog
    end

    def groom
      # Some complex grooming logic
    end
  end
end

This service was included by the groomer gem and will become a core piece of the application. To keep everyone in the project sane and well rested, a robust spec suite is needed.

Spec It Up

Using RSpec and the GroomingService, a very basic test can be written to make sure the code works as expected.

Introducing the GroomingService into the Dog class could look like:

class Dog
  def groom
    GroomingService::Groomer.new(self).groom
  end
end

The first test should be to make sure that when the groom method on a Dog is called, it initializes and calls groom out to the GroomService::Groomer object.

When asserting that a certain set of methods is called on a particular object, the built in RSpec double method is very handy.

The double method returns a stand-in object to assert allow and expect messages against. Any method that is called on the test double which is not explicitly allowed or expected results in an error.

describe Dog do
  subject { described_class.new }

  describe '#groom' do
    let(:groomer) { double(GroomingService::Groomer) }

    it 'initializes and calls groom' do
      expect(GroomingService::Groomer).to receive(:new) { groomer }
      expect(groomer).to receive(:groom)

      subject.groom
    end
  end
end

As expected, this test passes beautifully:

$: rspec dog_spec.rb

Dog
  #groom
    calls new with self and then groom

Finished in 0.00773 seconds (files took 0.09367 seconds to load)
1 example, 0 failures

That can be it, right? Pack it up and put the dogs away, whoever let them out will have to do it all again later.

Think of the Future

After some time passes, the GroomingService::Groomer has changed its method signature. Instead of the groom method, it has changed to groom!. Chances are someone thought that either the method had too many side affects or that ! are just really awesome looking.

In any case, the included service changes and when the test runs:

$: rspec dog_spec.rb

Dog
  #groom
    calls new with self and then groom

Finished in 0.00735 seconds (files took 0.10532 seconds to load)
1 example, 0 failures

It passes? How can that be? The method changed from groom to groom!. The groom method does not even exist on GroomingService::Groomer. It seems that the specs do not care what does or does not exist on the double, they just happily pass.

Use instance_double

The answer here is to use instance_double. Like double, instance_double also returns a stand-in object that raises errors if methods not allowed or expected are called on the object.

The key difference is that instance_double checks the underlying object to make sure that it responds to methods before making assertions that they are called.

describe Dog do
  subject { described_class.new }

  describe '#groom' do
    let(:groomer) { instance_double(GroomingService::Groomer) }

    it 'initializes and calls groom' do
      expect(GroomingService::Groomer).to receive(:new) { groomer }
      expect(groomer).to receive(:groom)

      subject.groom
    end
  end
end

Now running this test, we see the failure we should expect:

$: rspec dog_spec.rb

Dog
  #groom
    calls new with self and then groom (FAILED - 1)

Failures:

  1) Dog#groom calls new with self and then groom
     Failure/Error: expect(groomer).to receive(:groom)
       the GroomingService::Groomer class does not
          implement the instance method: groom

Finished in 0.0054 seconds (files took 0.08288 seconds to load)
1 example, 1 failure

While no one likes failing specs, a failure here is always preferable to one in production.

Since the API that we do not own (Groomer) changed, it was a mistake to blindly mock it without asking the underlying object if that method existed. However, with this problem corrected, the code is more robust and development can continue.

Mocking with Confidence

Asserting that a method is called on an object is a very common pattern in Ruby tests. Additionally, Ruby does not have the benefit of compiled languages in terms of method invocation validation. We as Rubyists rely on automated testing to make sure that our applications work as expected, can be iterated upon, and handle third party library updates. While it may be attractive to use permissive mocking assertions, they can be dangerous and cause more problems than they are worth.