---
title: Localized external services
teaser:
tags: web,testing
author: Mike Burns
published_on: 2010-05-13
---

Common situation: you need to hit an external API. Common solution: mock it out
in your tests using [Mocha](http://mocha.rubyforge.org/) or
[WebMock](http://github.com/bblimke/webmock). Common result: does not match
exactly what will happen.

### Why

See, the issue we've run into with method-level mocking is that we have to know
exactly which methods will be called and how Net::HTTP will be used at a low
level. This brittleness becomes apparent when we move from e.g. Net::HTTP to
[HTTParty](http://github.com/jnunemaker/httparty). The typical brittleness of
refactoring, plus it doesn't even do
[spies](https://thoughtbot.com/blog/post/159805295/spy-vs-spy) or support
mocking POST request params!

Another issue is that of simply wrapping our heads around things: using
[FakeWeb](http://fakeweb.rubyforge.org/)&mdash;a Net::HTTP stub
helper&mdash;requires repetitive code in our setups, files all over the
filesystem with our expected results, and poor encapulation in general.

Let's get it closer to reality and practicality using
[ShamRack](http://github.com/mdub/sham_rack).

### What

**We've used this for real apps**. Some example services: Facebook, OAuth,
OpenID, credit card processing.

### How

**ShamRack mounts a [Rack](http://rack.rubyforge.org/) app locally, just for
your tests.** It goes one further: it "mounts" it using Net::HTTP such that
requests to the Rack app never hit any network. Not even the local one. It's
sneaky like that.

**The example** we'll work with here is: you are writing an app that needs to
display the current copyright on the Web page, at all times. You can't trust
your local server for this either; you need to hit an external date service.

This external date service has an <abbr title="Application Programming
Interface">API</abbr> so let's write a test to make sure it exists:

`date_server/features/get_date.feature`:

    Feature: Getting a date

      Scenario: Request the current date
        Given the date is "2010-05-13"
        When I GET to "/date"
        Then I receive a HTTP 200
        And I should see "//dates/date[@now='2010-05-13']" in the response XML

The step definitions for this are fairly interesting:

`date_server/features/step_definitions/api_steps.rb`:

    require 'nokogiri'

    Given 'the date is "$date"' do |date_string|
      Date::DateStore.set_date_to(Date.parse(date_string))
    end

    When 'I GET to "$path"' do |path|
      url           = URI.parse("http://#{DATE_HOSTNAME}#{path}")
      request       = Net::HTTP::Get.new(url.path)

      @response = Net::HTTP.new(url.host, url.port).start do |http|
        http.request(request)
      end
      @parsed_response = Nokogiri::XML(@response.body)
    end

    Then 'I receive a HTTP 200' do
      @response.code.to_i.should == 200
    end

    Then 'I should see "$xpath" in the response XML' do |xpath|
      @parsed_response.xpath(xpath).should_not be_empty, "could not find #{xpath} in: #{@response.body.inspect}"
    end

We parse the response using Nokogiri and check the response using
`#xpath`. But we're doing a Net::HTTP request; we need ShamRack
mounted:

`date_server/features/support/env.rb`:

    DATE_ROOT     = File.join(File.dirname(__FILE__), '..', '..')
    RAILS_ROOT    = File.join(DATE_ROOT, '..')
    DATE_HOSTNAME = 'example.com'

    $:.unshift(File.join(RAILS_ROOT, 'vendor', 'gems', 'crack-0.1.6', 'lib'))
    $:.unshift(File.join(RAILS_ROOT, 'vendor', 'gems', 'sham_rack-1.2.1', 'lib'))
    $:.unshift(File.join(RAILS_ROOT, 'vendor', 'gems', 'sinatra-0.9.4', 'lib'))

    require 'sham_rack'
    require 'spec/expectations'
    require File.join(DATE_ROOT, 'dates')

    Dir[File.join(DATE_ROOT, 'lib', '*.rb')].each do |filename|
      require filename
    end

    ShamRack.at(DATE_HOSTNAME).rackup do
      run DateServer
    end

    After do
      Date::DateStore.clear!
    end

Lots of mention of `Date::DateStore` now, including
  `#clear!` and `#set_date_to` methods.

`date_server/lib/date_store.rb`:

    class Date::DateStore
      @@the_date = nil

      def self.set_date_to(a_date)
        @@the_date = a_date
      end

      def self.current_date
        Date.parse(@@the_date.to_s)
      end

      def self.clear!
        @@the_date = nil
      end
    end

This class is namespaced so you don't confuse it with anything in the main
  app. This bit me so watch out for it.

We can change to the `date_server/` directory and run
  `cucumber` but it's more fun to have a profile for it:

`config/cucumber.yml`:

    dates: --format progress --strict --tags ~@wip date_server/features

So now we can run: `cucumber -p dates`

Cool but now the date server tests are failing. Make 'em pass with
  Sinatra!

`date_server/dates.rb`:

    require 'builder'
    require 'sinatra'
    require 'crack'

    class DateServer < Sinatra::Base
      get "/date" do
        current_date = Date::DateStore.current_date

        status 200
        builder do |xml|
          xml.instruct!
          xml.dates do |dates|
            dates.date :now => current_date.strftime('%Y-%m-%d')
          end
        end
      end
    end

Sweet, we have a date server! It uses <abbr title="Extensible Markup
Language">XML</abbr> because <abbr title="Extensible Markup Language">XML</abbr>
is a solution to any problem, even one that doesn't exist.

So back in the main app we can write our Cucumber test:

`features/view_home_page.feature`:

    Feature: Look at the home page

      Scenario: User views the home page
        Given the date server is set for "2008-04-16"
        When I go to the home page
        Then I should see "Copyright 2008"

Most of that test uses Webrat but the setup pokes into the
  `Date::DateStore`.

`features/step_definitions/date_server_steps.rb`:

    Given 'the date server is set for "$date"' do |date_string|
      Date::DateStore.set_date_to(Date.parse(date_string))
    end

Well that needs the `Date::DateStore` class, so load 'er up!

`features/support/dates.rb`:

    require 'date_server/lib/date_store'
    require 'date_server/dates'

    ShamRack.at(DATE_HOSTNAME).rackup do
      run Sinatra::Application
    end

    Before do
      Date::DateStore.clear!
    end

The rest is making the tests pass:

`config/routes.rb`:

    ActionController::Routing::Routes.draw do |map|
      map.root :controller => 'homes', :action => 'index'
    end

`app/controllers/homes_controller.rb`:

    class HomesController < ApplicationController
      def index
        current_date = DateModel.current
        render :text => "Copyright #{current_date.year}"
      end
    end

`app/models/date_model.rb`:

    class DateModel
      def self.current
        url = URI.parse("http://#{DATE_HOSTNAME}/date")
        res = Net::HTTP.start(url.host, url.port) {|http| http.get(url.path)}
        parsed_xml = Nokogiri::XML(res.body)
        Date.parse(parsed_xml.root.at('date').attribute('now').value)
      end
    end

### Yeah

That Sinatra app is re-usable now, for your other projects that need a date
server. You can bundle it as a gem, package step definitions with it, write a
hook to run the tests for it while running your test suite, or to run it when
you run `script/server`. The encapsulation opens you to more possiblities!
