There seems to be a tendency to stub or mock classes when writing integration tests for basic http services. I’m actually not a big fan of this approach. By definition, the integration test should truly integrate with another subsystem. In the case of a http service, the tests should probably integrate over http, agreed?
Here’s one approach for testing services without stub or mocks…
Imagine a reservation booking system that integrates with a 3rd party API. By default, you might create a Rails model that extends ActiveModel, ActiveRecord or even ActiveResource. Although you stop, after reading this blog post, and create an unbuilt Gem that reaches out to the 3rd party service. Your service might look something like this…
require "rack"
require "nokogiri"
class ReservationService
def create_reservation(reservation)
url = URI.parse('http://localhost:9393/')
http = Net::HTTP.new(url.host, url.port)
response, body = http.post(url.path, reservation.to_xml, {'Content-type' => 'text/xml; charset=utf-8'})
reservation = Reservation.from_xml(body)
reservation
end
end
More important, your immutable model might look something like this. (Note: I’m not inheriting from a Active* base class, although I’ll save the inheritance discussion for another blog post)
class Reservation
attr_reader :name, :date, :duration, :booked
def initialize(name, date, duration, booked = false)
@name = name
@date = date
@duration = duration
@booked = booked
end
def to_xml
doc = <<XML
<?xml version="1.0" encoding="UTF-8"?>
<reservations>
<reservation>
<name>#{@name}</name>
<date>#{@date}</date>
<duration>#{@duration}</duration>
<booked>#{@booked}</booked>
</reservation>
</reservations>
XML
doc
end
def self.from_xml(xml)
doc = Nokogiri::HTML(xml)
doc.xpath("//reservation").each do |reservation|
reservation.children.each do |child|
case child.name
when "name"
@name = child.text
when "date"
@date = Date.parse(child.text)
when "duration"
@duration = child.text.to_i
when "booked"
@booked = eval(child.text)
end
end
end
Reservation.new(@name, @date, @duration, @booked)
end
end
Then you start writing tests. You stub the service, then you stub Typhoeus. You might pull Typhoeus and use Artifice or rack-test. Sure the approach works, although are you really testing the full integration (they do stub at the lowest level)?
You could argue a more complete integration test might include the http layer. One approach might be to fire up a simple rack handler that matches the API specification.
Here’s an example…
require "test/unit"
require "date"
require File.expand_path(File.join(File.dirname(__FILE__), "reservation_service"))
class TestServer
def initialize(response_code, response_body, response_headers = {})
@response_code = response_code
@response_body = response_body
@response_headers = response_headers
end
def start
@thread = Thread.new do
Rack::Handler::WEBrick.run(self, :Port => "9393", :Host => "localhost")
end
sleep 1
puts "started server."
end
def stop
Thread.kill(@thread)
puts "stopped server."
end
def call(env)
[@response_code, @response_headers, [@response_body]]
end
end
class TestService < Test::Unit::TestCase
def test_create_reservation
expected_reservation = Reservation.new("John Brown", Date.today, 3, true)
server = TestServer.new(200, expected_reservation.to_xml, {})
server.start
service = ReservationService.new
actual_reservation = service.create_reservation(Reservation.new("John Brown", Date.today, 3))
assert_equal(expected_reservation.name, actual_reservation.name)
assert_equal(expected_reservation.date, actual_reservation.date)
assert_equal(expected_reservation.duration, actual_reservation.duration)
assert_equal(expected_reservation.booked, actual_reservation.booked)
server.stop
end
end
(Typhoeus actually uses a similar approach via TyphoeusLocalhostServer.rb within their own test suite)
You might ask why not just integrate with the 3rd party’s API sandbox environment. Because doing so could impact test performance as well as your ability to run tests – you become too dependent on their service’s availability.
A similar approach might be to VCR, although VCR might not work without an actual sandbox environment.
About the Author