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.


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
    case response
    when Net::HTTPSuccess then
      logger.info "Hoptoad Success"
      logger.error "Hoptoad Failure"

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

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

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

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

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!

author image
Mike Burns

Pair with one of our expert developers to level up your skills with Coaching by thoughtbot. Save time learning best practices and techniques for reducing technical debt in Ember, Ruby, Haskell, and Go in 1-on-1 sessions tailored to your goals.