Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker

Testing Ruby Services without Mocks

Mike Barinek
Wednesday, March 7, 2012

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.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

4 Comments

  1. Joe Van Dyk says:

    No no no no… you want to use VCR.

    March 7, 2012 at 9:58 pm

  2. Mike Barinek says:

    here’s a small update…

    require “test/unit”
    require “date”

    require ‘net/http’
    require ‘uri’

    require File.expand_path(File.join(File.dirname(__FILE__), “reservation_service”))

    class HttpTestCase < Test::Unit::TestCase
    def respond_with(response_code, response_body, response_headers = {})
    @response_code = response_code
    @response_body = response_body
    @response_headers = response_headers
    end

    def setup
    @thread = Thread.new do
    Rack::Handler::WEBrick.run(self, :Port => “9393″, :Host => “localhost”)
    end
    wait_for_server
    end

    def wait_for_server
    Timeout::timeout(10) do
    loop do
    begin
    response = Net::HTTP.get_response(URI.parse(“http://localhost:9393″))
    break if response.is_a?(Net::HTTPSuccess)
    rescue SystemCallError => error
    end
    end
    end
    puts “started server.”
    rescue Timeout::Error => error
    abort “Servers never started!”
    end

    def teardown
    Thread.kill(@thread)
    puts “stopped server.”
    end

    def call(env)
    [@response_code ||= 200, @response_headers ||= {}, [@response_body ||= ""]]
    end

    end

    class TestReservationService < HttpTestCase
    def test_create_reservation
    expected_reservation = Reservation.new(“John Brown”, Date.today, 3, true)

    respond_with(200, expected_reservation.to_xml, {})

    service = ReservationService.new
    reservation = Reservation.new(“John Brown”, Date.today, 3)
    actual_reservation = service.create_reservation(reservation)

    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)
    end
    end

    March 8, 2012 at 2:12 pm

  3. isabel marant jenny boots says:

    urrtlopserty
    Shiny lilac suede sewn light household leather is often identical, nevertheless applying dense pattern, even so the hindfoot is usually supplied from the teeny laces intended for beautification; Shoelace continues to position hole, light curtains a compact document, the main points usually are generally gentle.

    May 25, 2012 at 1:14 am

  4. Isabel Marant Boots says:

    iioopprrtteeww
    The particular reddish dots inside white-colored buckskin foundation, regular sewing buckskin shade, the particular automobile accident regarding delightful and also attractive. The particular transforming type high heels helps it be thus captivating in which it is able to bring in every person. The particular dip inside the tiny department of transportation features a got in, and in addition along with a great attractive bohemian. Can be found in the particular useless get more entertaining to be able to shade lock up and also increase more details to be able to Isabel Marant shoes or boots beautification.

    May 28, 2012 at 7:28 pm

Add New Comment Cancel reply

Your email address will not be published.

Mike Barinek

Mike Barinek
Boulder

Recent Posts

  • American Thrombosis and Hemostasis Network (ATHN) is looking for a Web Application Developer
  • Portico is looking for a Web Application Developer
  • Rails Contained: A Container for Web Application Development and Deployment
Subscribe to Mike's Feed

Author Topics

boulder (4)
jobs (3)
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >