Better Mocking in Ruby

Jared Carroll ·

In TDD we write tests to discover the interface of the object we’re testing. Mock objects extend TDD by discovering an object’s collaborators. [1] By mocking an object’s collaborators we can truly test an object in isolation. Changes in the implementation of its collaborators, but not their interface, will not cause a test in which those collaborators are mocked out to fail. This results in a more robust test suite in which a single failure doesn’t result in rippling failures throughout the suite.

Here’s a few tips I’ve learned that are helpful when mocking.

Never Mock Without a Failing Integration Test

This is the most important rule of mocking. When you test using mocks you are replacing an object’s collaborators with fakes. If the interface of those collaborators change, your test that mocks those collaborators could pass even though its incorrect, a false positive. In order to avoid such a situation you have to start with a failing integration test.

This could be a Cucumber feature, an RSpec request spec, or maybe even a controller spec, as long as it adheres to one rule: it tests all the objects with real collaborators i.e. nothing is mocked out. [2] Once this test is written you can then drop down through the layers of your application testing each object in isolation with mocking. The integration test is your safety net to catch failures between your objects due to interface changes.

Only Mock Types That You Own

Never mock code you didn’t write such as frameworks and 3rd party libraries. You want to actually exercise this code to see it work as advertised and you’re integrating with it correctly. Let’s take a look at a feature that we’ll implement using some 3rd party code: “User search uses google.com to perform the search”

After drilling down from a failing integration test to our model, our first test might be something along the lines of: [3]

describe User do

  describe '.search' do
    it 'searches Google for the given query' do
      query = 'foo'
      results = []

      HTTParty.
        should_receive(:get).
        with('http://www.google.com',
             :query => {
               :q => query,
               : output => 'json'
             }).
        and_return(results)

      User.search query
    end
  end

end

Here we’re mocking HTTParty, a type we don’t own. Unfortunately this prevents us from refactoring User.search to use another HTTP library. How we can fix this? By mocking roles not types.

Mock Roles Not Types

Let’s rewrite this by not mocking HTTParty and instead introduce a role specific to our domain.

describe User do

  describe '.search' do
    it 'searches for the given query' do
      query = 'foo'
      results = []

      searcher = double 'searcher'
      searcher.
        should_receive(:search).
        with(query).
        and_return(results)

      User.searcher = searcher

      User.search query
    end
  end

end

Here we introduced a searcher role with a generic API consisting of a search method that takes a query. We also renamed the spec to be more generic by not mentioning Google, an implementation detail. We keep the original spec as our high-level integration spec.

By introducing a domain specific role, we’ve removed any mention of implementation and now have a more generic domain model. If we want to refactor User.search to use a different HTTP library we configure User with a different implementation of the searcher interface.

Our new test did require us to add a setter to our User class in order to configure its searcher dependency.

class User < ActiveRecord::Base

  class_attribute :searcher

  def self.search(query)
    searcher.search query
  end

end
&#91;/sourcecode&#93;

<h2>Testing 3rd Party Integration</h2>

Now that we've replaced our 3rd party dependency with a domain specific role how do we test our new role implementations?  We'll continue to follow our rule of not mocking types we don't own and instead test them with mini-integration tests.  This is ok because it's the only way to ensure 3rd party behavior and integration.

Here's a non-mocked test for one implementation of our <code>searcher</code> role that we'll call <code>Google</code>:


describe Google do

  describe '.search' do
    before do
      query = 'foo'
      body = [{ 'url' => 'http://foo.com'    },
              { 'url' => 'http://foobar.com' }]
      stub_request(:get, 'www.google.com').
        with(:query => {
               :q => query,
               : output => 'json'
             }).
        to_return(:headers => { 'Content-Type' =>'application/json' },
                  :body => body.to_json)

      @results = Google.search query
    end

    it 'searches google for the given query' do
      @results.should have(2).results
    end
  end

end

This test doesn’t mock any 3rd party code, giving us freedom to refactor in the future. However we do use webmock to stub out a low level HTTP request but we don’t specify how this request is actually made so we can still refactor how we end up making this request.

Here’s one possible implementation using HTTParty:

class Google

  def self.search(query)
    HTTParty.get 'http://www.google.com', 
                :query =>{ :q => query, : output => 'json' }
  end

end

The Tradeoffs of Mocking

Most of the backlash against mocking results from failure to follow the above suggestions. This makes refactoring without breaking a test very difficult. Following the above techniques can give us a more robust and refactorable test suite. Unfortunately this is achieved via additional layers of indirection using roles and dependency injection.

Is this flexibility/complexity necessary? Dependency injection never caught on in Ruby, even using simple setters with default arguments. What do we risk if we don’t mock? We risk every test essentially becoming an integration test, which gives us a brittle and slow test suite. Is this a bad thing?

So when do we mock? It depends. In the end mocking just becomes yet another tradeoff we have to consider when testing software.


[1] Mocks are also useful for removing external dependencies, simulating error conditions, etc.
[2] Actually some mocking is ok in integration tests e.g. you may mock to eliminate a dependency on a 3rd party API in order to have more consistent and faster test runs.
[3] All mocking examples use RSpec’s built in mocking and stubbing framework.