be nice to ducks or I will mock you
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 Ruby, Seattle.rb |
Trackbacks<
Use the following link to trackback from your own site:
http://plasti.cx/trackbacks?article_id=829
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).
01/24/2008 at 03:49PM
Fixed, thanks @pjm
01/30/2008 at 12:52PM
I liked it better when you were shouting PERL. :\ Nice post!