16 September 2008

Mocked by Default, but Unmocking in Some Cases with RSpec

Uh, ya, another great blog title, but we'll get over it. We use geocoding in our app, and that's a relatively costly operation time wise, especially when you may be doing it hundreds or thousands of times when your test suite runs. I can't stub out the objects that use it in many cases, so I wanted to stub out the actual geocode call unless I truly needed real geocoding (which is only when I'm testing the actual geocoding itself, and thus is a very small part of the test suite).

We use RSpec Specs and Stories and I wanted to mock out the geocoding by default, but unmock it in a few places. I asked about this on the mailing list, Googled and so on, but didn't find a solution that was working. So here is what I wound up doing...

In my spec_helper.rb file, I added:


Spec::Runner.configure do |config|
config.before(:each) do
# Setup fake geocoding unless told not to
unless @do_not_mock_geocoding
fake_geocode = OpenStruct.new(:lat => 123.456, :lng => 123.456, :success => true)
GeoKit::Geocoders::MultiGeocoder.stub!(:geocode).and_return(fake_geocode)
end
end
end


What this does is mock the geocoding unless a test has set the @do_not_mock_geocoding variable to true. One caveat, at least from what I've found, is that you need to set that to true in a before(:all) block in your tests, so that it happens before the before(:each). This is minor, as you can just have something like:

describe "with real geocoding" do
before(:all) do
@do_not_mock_geocoding = true
end

# your tests that want real geocoding
end


The impact this has had on our test suite is tremendous. I had already had some partial mocking of the geocoding in place, but was sweeping the system to put it in because the time it took to run our test suite was out of hand at about 13 minutes! Now that I've got this in, it runs in 2 minutes! Geocoding is used in two of our most core objects, which is why it has such a big impact on the test suite. This is one place mocking has really proved to be a massive value!

7 comments:

Anonymous said...

I stumbled over the same problem and tried out your solution... but the stubbing doesn't work for me.

In the specs the GeoKit::Geocoders::MultiGeocoder.geocode call goes directly to the module, instead of the stub.

My specs looks similar to yours... and I'm doing the call in an before_save filter in the model.

What do I miss?

Unknown said...

Mike, I'm not really sure. Make sure your spec_helper.rb is included for all your tests. I too do my geocoding in a before_save. And, obvious, but just in case, make sure you are NOT setting @do_not_mock_geocoding anywhere you don't want it mocked.

Anonymous said...

Argh... I found the error. Using update_attribute in a before_save filter is really kind of stupid! :-(
Mocking and stubbing worked fine, it was just the endless loop!

Thanks for your support anyway!

Anonymous said...

A good article

ngọc trần said...

very good...

Unknown said...

Thanks for writing this up! There's a typo in the 'turn geocoding back on' part, though: you need a 'do' after the 'before(:all)'.

Chris said...

Ryan, thanks for the comment. I added in that missing 'do' as well :)