Plasticx Blog

Capable of being shaped or formed

be nice to ducks or I will mock you

Posted by Mike 01/24/2008 at 12:18PM

When I first started programming Ruby I lurked on the Seattle.rb mailing list and then gathered the courage to finally attend a meeting. At the time I had written a Perl script that did the basics of what MMS2R does and it was lucky for me that Aaron Patterson who coincidently maintains WWW::Mechanize was also a member of Seattle.rb. My Perl script was using Perl’s original version of WWW::Mechanize to scrape pictures from MMS that were sent by mobile subscribers to Sprint.

I knew that it would be a great experience to me if I converted the Perl script into a Gem. I would be contributing to the Ruby community, I would be learning how to author a Gem, and I would improve on my coding and testing abilities.

A testing problem I was perplexed with was how should I test my code that reaches out to Sprint’s content servers to scrape images? The other cellular carriers actually deliver their images mime-encoded in the MMS payload so testing them in a closed environment is simple enough with a fixture.

At that time I had noticed Eric Hodel’s releases of rc-rest and asked him to show me how he handled testing code that in production would be making calls out over the wire. I haven’t asked Eric his opinion but he basically duck rapes (duck raping – rudely duck type by any means necessary) URI, URI::HTTP, Net::HTTP, Net::HTTPResponse in the rc-rest code so that he can test it without actual network connections being opened.

I followed his example and it worked for me for a time but something about my code didn’t seem “nice”. For one thing I now had extra code to maintain just to maintain my tests. That code was opening up Ruby library code and there was alway the possibility that I was changing the behavior of the library in some way.

Then I stumbled upon FakeWeb but it only supported caging HTTP GET requests. So I patched it to handle all four HTTP verbs GET POST PUT DELETE. My patch wasn’t accepted but you can get my work here a better FakeWeb, v. 1.2.0 Even so, FakeWeb was doing what Eric was doing in a generic way, duck raping Net::HTTP. Be nice to ducks!

After RailsConf 2007 mocking was getting lots of attention with the likes of FlexMock, Mocha, and the beginnings of rspec. Once I learned those techniques a bit more it was clear that mocking was the best strategy for testing code that relies on Ruby libraries to open network connections.

Here is a contrived example of testing code that relies on Net::HTTP. As you can see Net::HTTP is opened up and all requests through it raise an exception. However, we do that to make sure our tests are mocking our code correctly. If a mock doesn’t handle a URI that our code tries to call an error is raised by Net::HTTP. This places the emphasis of our test at the point of input to and output from Net::HTTP#get_response We don’t have to go over the wire to have meaningful tests.

test_helper.rb

require 'net/http'
require 'net/https'require net/http
require net/https

# patch Net::HTTP so un caged requests don’t go over the wire
module Net #:nodoc:
class HTTP #:nodoc:
alias :old_net_http_request :request
alias :old_net_http_connect :connect

def request(req, body = nil, &block) prot = use_ssl ? "https" : "http" uri_cls = use_ssl ? URI::HTTPS : URI::HTTP query = req.path.split(?,2) opts = {:host => self.address, :port => self.port, :path => query[0]} opts[:query] = query[1] if query[1] uri = uri_cls.build(opts) raise ArgumentError.new("#{req.method} method to #{uri} not being handled in testing") end def connect raise ArgumentError.new("connect not being handled in testing") end end

end

test_benice.rb

require File.join(File.dirname(__FILE__), "test_helper")
require 'test/unit'
require 'uri'
require 'net/http'
require 'rubygems'
require 'mocha'require File.join(File.dirname(FILE), "test_helper")
require test/unit
require uri
require net/http
require rubygems
require mocha

class TestBeNice < Test::Unit::TestCase

def test_request_should_be_caged uri = URI.parse(http://plasti.cx/) res = mock(:content_type => text/html, :code => 200, :body => <html><body>my blog</body></html>) Net::HTTP.stubs(:get_response).once.with(uri).returns res blog = URI.parse(http://plasti.cx/) assert_nothing_raised do res = Net::HTTP.get_response(blog) assert_equal 200, res.code assert_equal text/html, res.content_type assert_match /my blog/, res.body end end def test_get_request_is_not_caged url = URI.parse(http://ruby-lang.org/) assert_raise(ArgumentError) do Net::HTTP.get_response(url) end end def test_all_net_http_is_caged url = URI.parse(http://rubyforge.org/) assert_raise(ArgumentError) do res = Net::HTTP.start(url.host, url.port) do |http| http.get(/) end end end

end

Posted in , |

Trackbacks<

Use the following link to trackback from your own site:
http://plasti.cx/trackbacks?article_id=829

  1. pjm
    01/24/2008 at 03:38PM

    Please stop shouting “PERL”! It’s not an acronym (even if a few have been retrofitted over the years).

  2. monde
    01/24/2008 at 03:49PM

    Fixed, thanks @pjm

  3. Jordan Isip
    01/30/2008 at 12:52PM

    I liked it better when you were shouting PERL. :\ Nice post!


Web Statistics