A HTTP Testing Proxy

Mike Burns

Hoptoadwhich is now live—is both an application and a Rails plugin that must work together. This integration simply cannot go untested in a test-happy place like thoughtbot.

Battletoads

The plugin, I’m sure you’ve seen, has a private method #send_to_hoptoad that handles the dirty HTTP stuff. It looks like a more complicated version of this:

def send_to_hoptoad(data)
  url = HoptoadNotifier.url
  Net::HTTP.start(url.host, url.port) do |http|
    headers = {
      'Content-type' => 'application/x-yaml',
      'Accept' => 'text/xml, application/xml'
    }
    response = begin
                 http.post(url.path, stringify_keys(data).to_yaml, headers)
              rescue TimeoutError => e
                 nil
               end
    case response
    when Net::HTTPSuccess then
      logger.info "Hoptoad Success"
    else
      logger.error "Hoptoad Failure"
    end
  end
end

Dead frog controlled via a network

The integration test simulates the plugin actually hitting the application. Normally to test #send_to_hoptoad you’d use Mocha to stub out Net::HTTP methods, but stubbing sweeps away too many potential issues here.

What we really want is an integration test that pits the plugin against the real application, without running a server. We want Net::HTTP#post to use ActionController::Integration::Session#post .

The gruesome internals

In the integration test for the application, first require in the needed tricks:

require 'test_helper'
require 'net/http'
require File.dirname(__FILE__) + '/../lib/hoptoad_notifier/lib/hoptoad_notifier'

(we’ve installed a copy of the plugin into test/lib)

Then, open up Net::HTTP and get rid of the bits that connect to the network. This part could be done with Mocha, but we need to open Net::HTTP later so we might as well do it this way:

class Net::HTTP < Net::Protocol
  def connect
  end
end

While you have Net::HTTP open, replace #post with a proxy. The class to proxy to is passed into the proxy_object module variable.

class Net::HTTP < Net::Protocol
  mattr_accessor :proxy_object

  def post(path, body, headers)
    self.class.proxy_object.post path, body, headers
  end
end

Finally in the test setup block we need to initialize Net::HTTP with the appropriate instance of ActionController::Integration::Session (which is to say, self):

class PostingFromHoptoadNotifierTest < ActionController::IntegrationTest
  context "with a connection from the plugin to the application" do
    setup do
      Net::HTTP::proxy_object = self
    end

    should_eventually "deny access to people who disagree with me" do
    end
  end
end

All #should statements inside the context will proxy themselves through the integration test instead of hitting the network. Bam!

Check out the complete test file.

Look ma, no OSI layer 7!