GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Have You Ever... Faked It?

I’ll admit it - I’ve faked it. Sometimes, you just can’t wait for a service to finish and you just want to fake a satisfactory response. There are lots of techniques for doing this: stubs, mocks, spies, and fakes. A full fake object will require more up-front effort than a quick stub, but they can be more reusable, reliable, and fail-proof.

Keeping your tests local

Probably the first reason every developer encounters that drives them to test doubles of any kind is an external service. Writing tests that interact with a live server is bad for a number of reasons: it’s slow, it’s hard to setup your test, and your tests will likely interact with each other (or other developers' tests). You’ll also eventually run into that frustrating situation where server downtime results in a development blackout.

Overriding methods

Ruby is extremely flexible with its class definitions, to the point that you’re allowed to reopen and append (or redefine) methods at any point. I’ve seen lots of projects where this is used to simply white out pieces of the application that developers don’t want running in tests:

# The production code
# app/models/event.rb

class Event < ActiveRecord::Base
  before_validation :geocode
  # ...
  private
  def geocode
    geo = GeoKit::Geocoders::MultiGeocoder.geocode(address)
    if geo.success
      self.lat, self.lng = geo.lat, geo.lng
    else
      self.errors.add(:address, 'Unable to identify your location.')
      false
    end
  end
end

# The override
# test/support/geocoding.rb

module GeoKit
  module Geocoders
    class MultiGeocoder < Geocoder
      def self.geocode(location)
        loc = GeoLoc.new
        loc.lat = 1
        loc.lng = 1
        loc.success = true
        loc
      end
    end
  end
end

That little snippet lets you hit the ground running. Your tests don’t need to hit an external server, and you can test that a latitude and longitude is assigned when the record is created. However, you can’t test the negative case, and if you want to write tests for geo-spatial search, you’re out of luck.

Using stubs

If you need different geocoded results in different tests, a straight-up override won’t do it for you. At this point, a developer might turn to stubs and mocks:

describe Event do
  it "should geocode a valid location" do
    loc = GeoLoc.new
    loc.lat = 100
    loc.lng = 200
    loc.success = true
    GeoKit::Geocoders::MultiGeocoder.
      stubs(:geocode).
      with('123 Happy Street').
      returns(loc)
    event = Factory.build(:event, :address => '123 Happy Street')
    event.save
    event.lat.should == loc.lat
    event.lng.should == loc.lng
  end

  it "should add an error message with an invalid location" do
    loc = GeoLoc.new
    loc.success = false
    GeoKit::Geocoders::MultiGeocoder.
      stubs(:geocode).
      with('123 Sad Lane').
      returns(loc)
    event = Factory.build(:event, :address => '123 Sad Lane')
    event.save
    event.should_not be_valid
  end
end

You’ll probably need to stub out geo locations several times throughout the test suite, so wrapping these up in helper methods helps:

describe Event do
  it "should geocode a valid location" do
    loc = build_geoloc(:lat => 100, :lng => 200, :success => true)
    stub_geoloc!('123 Happy Street' => loc)
    event = Factory.build(:event, :address => '123 Happy Street')
    event.save
    event.lat.should == loc.lat
    event.lng.should == loc.lng
  end

  it "should add an error message with an invalid location" do
    loc = build_geoloc(:success => false)
    stub_geoloc!('123 Sad Lane' => loc)
    event = Factory.build(:event, :address => '123 Sad Lane')
    event.save
    event.should_not be_valid
  end
end

However, this leads to a few new problems:

  • Most of the time you create an Event in a test, you don’t care about geocoding, so you’ll probably still need to add an override.
  • Writing stubs like this in Cucumber tests is tricky - you need to make sure your stubs are torn down, and refactoring your implementation will noisily cause scenarios to fail. This isn’t what you want from an integration test.
  • You can’t test the stubs themselves, and these methods are difficult to reuse, especially between projects.

Swappable components

One of the best pieces of programming advice I’ve ever received is this: “separate the pieces that change from the pieces that don’t.” Logic that doesn’t directly apply to your domain, such as geocoding and credit card processing, are likely to change. Which provider will you use? Do you need failover support? Adding the code (and tests) to your models is likely to increase churn and noise in the essential models of your application. Extracting these other concerns to external components helps reduce this noise. Writing these components so that the implementation can be swapped out will take you even further.

Luckily, in the case of GeoKit, this has all been done for you. GeoKit supports several Geocoders, and the list of Geocoders that will be used can be configured per-environment. The list of Geocoders is configured via the global “provider_order” setting:

GeoKit::Geocoders::provider_order = [:fake]

GeoKit will look for a camelized constant nested under GeoKit::Geocoders for each provider specified, so you can write a fake geocoder like this:

module GeoKit
  module Geocoders
    class FakeGeocoder < Geocoder
      def self.geocode(location)
        # return a GeoLoc instance
      end
    end
  end
end

If you maintain a global registry of locations mapped to latitude and longitude, you can write steps like the following:

Given /^the following geolocations:$/ do |table|
  table.hashes.each do |hash|
    location = hash['Location']
    lat = hash['Lat'].to_f
    lng = hash['Lng'].to_f
    GeoKit::Geocoders::FakeGeocoder.locations[location] = [lat, lng]
  end
end

Because GeoKit allows you to swap out the geocoder implementation without changing your model code, there’s no test-specific code in your model, no overrides to look for, and no extra churn in your model if you decide to switch providers.

Keeping it fresh

One potential issue with the above faking strategy is that there are a number of globals involved: the GeoKit provider is specified globally, as is the registry of fake locations. This means that you’ll need a teardown phase to clear the registry. In this case, there’s little reason to swap out implementation at runtime or between tests. However, if you’re faking out something that changes live, these global won’t do.

When possible, I recommend accepting the external component as a parameter. If Event could take a GeoKit provider as a parameter, you could write tests like the following:

describe Event do
  it "should geocode a valid location" do
    fake_geocoder = FakeGeocoder.new('123 Happy Street' => [100, 200])
    event = Factory.build(:event, :address => '123 Happy Street',
                                  :geocoder => fake_geocoder)
    event.save
    event.lat.should == 100
    event.lng.should == 200
  end
end

There’s no teardown required. The FakeGeocoder is discarded after the test executes, so the mapped locations don’t live beyond the test.

Keeping it real

When faking out a component, make sure you adequately reproduce the actual expected behavior. When faking out payment gateways, you’ll need to cover a variety of error responses and so on. When faking out a geocoder backend, you may want to simulate timeout errors and other failures. Make sure you have enough test cases that your application will catch the edge cases that occur when using the real thing.

Apply liberally

Developers are forced to extract code into external components when it’s just not feasible to use the same code in the tests, as is the case when contacting external services. However, there are many pieces of behavior that can be extracted that don’t need to be: searching, authentication, and so on.

Although developers usually only use stubs and mocks when they need to, many developers eventually prefer the isolation, speed, and declarative nature of such doubles. If you start experimenting with fakes in place of mocks for external services, you may find that there are pieces of your code that you could swap out just to keep the churn down in your models, or to keep your tests focused on what they’re testing.

How about you? Have you ever…faked it?

What’s next?

If you found this useful, you might also enjoy: