Mock the web – OpenURI

On one of our projects, we fetch several blog and twitter entries and build a little sidebar of this content. To do this, we have a controller endpoint that makes several http calls to remote services that grab these feeds. After requiring ‘open-uri’ in our controller, we can simply call open(url) and pass a block to process the feed provided at that url.

While writing my tests around this functionality, I realized that the test code was calling out to twitter and fetching data. That’s no good. So we need to stub out that call. Easy right? We can just stub the open() method and return some dummy data.

Not so fast. Where is the open function? Because we’ve required ‘open-uri’, the Kernel.open method has been aliased away and re-written to handle url inputs.  In our code, we’re using a block to process the results of open().  This block calls ‘read’ on those results.

class MyController  [{:channel => {:title => 'this is the channel title'}].to_json )
      Kernel.stubs(:open).yields( mock_readable )
      get :fetch_feed, :url => 'http://that_blog.blogspot.com/feeds/posts/default?alt=rss'
    end
    it 'assigns feed data to @feed_result' do
      assigns(:feed_result).channel.title.should == 'this is the channel title'
    end
  end
end

This spec fails. As it turns out, ‘open’, though defined on the Kernel module, is mixed in to our controller and we need to stub the call at that level to get the test to work. A quick mod makes this do the right thing:

describe MyController do
  context '#fetch_feed' do
    before do
      #stub out http calls
      mock_readable = stub(:read => [{:channel => {:title => 'this is the channel title'}].to_json )
      MyController.stubs(:open).yields( mock_readable )
      get :fetch_feed, :url => 'http://twitter.com/humblebrag'
    end
    it 'assigns feed data to @feed_result' do
      assigns(:feed_result).should include 'this is a feed entry'
    end
  end
end

And we’re off to the races. Apart from testing the parsing logic in our controller method, we now have a spec that is no longer calling out to an external service. Tests that require internet connectivity are not only slow and unreliable, they are just a bad idea. We trust that OpenURI has been properly tested and knows how to talk to the net. We should only test the functionality we are writing: namely the method that processes the results of our call to open(). We shouldn’t make our specs dependent on the implementation of open() nor a connection to an external URL.

The take-away lesson for me was: once a method is mixed in, the module it came from is unimportant. The mixed-in methods live on the class in which they’ve been mixed. Therefore, when testing, we should stub methods in this top-level class, not the base module where the method is defined.

As a little postscript, FakeWeb is, perhaps, an easier way to stub out the web when trying to write these tests. When it’s time to refactor, I’m going to take a look at putting that gem in my test bundle.

Advertisements