---
title: Using Capybara to Test JavaScript that Makes HTTP Requests
teaser:
tags: web,javascript,testing
author: Joe Ferris
published_on: 2012-11-01
---

![Complicated](http://media.tumblr.com/tumblr_mcrxy00UgZ1qzocnw.jpg)

Testing an application that integrates with an HTTP service can be tricky:

* Making a connection to another server is slow.
* Your tests can become dependent on data that lives outside the test suite.
* Going offline means you can't run the tests.
* The service going down causes continuous integration to fail.

It may seem like a [world of pain](http://www.youtube.com/watch?v=rrykk7E_2i0),
but you're not going to let a few HTTP requests get between you and your TDD,
are you?

## Story Time

Let's say you're making an internal dashboard for your site, which allows you
to view key health metrics. Among other things, you want to display the current
status of the build, so that you know whether or not it's safe to deploy. Your
build runs on a third party service, so you need to query their API.

## From the Top

You start with an acceptance test:

    feature 'health dashboard' do
      scenario 'view health dashboard' do
        create_passing_build
        sign_in_as_admin

        view_health_dashboard

        page.should have_passing_build
      end

      def create_passing_build
        FakeContinuousIntegration.stub_build_message(passing_build_message)
      end

      def view_health_dashboard
        visit '/admin/health_dashboard'
      end

      def have_passing_build
        have_content(passing_build_message)
      end

      def passing_build_message
        'All 2,024 tests passed.'
      end
    end

The test immediately fails because of your missing fake, and you <abbr title="Test Driven Development">TDD</abbr> your way into this simple class:

    class FakeContinousIntegration
      def self.stub_build_message(message)
        @@build_message = message
      end
    end

Your testing loop leads to this controller action:

    def show
      @latest_build_message = ContinousIntegration.latest_build_message
    end

## Details Emerge

At this point, it's time to drop down into a unit test. After a few cycles, you end up with this test:

    describe ContinousIntegration, '.latest_build_message' do
      it 'parses the build message from the CI server' do
        message = 'Great success'
        response = { 'message' => message }.to_json
        Net::HTTP.stubs(get: response)

        result = ContinousIntegration.latest_build_message

        Net::HTTP.should have_received(:get).with('buildserver.com', '/latest')
        result.should == message
      end
    end

And the implementation emerges:

    class ContinousIntegration
      HOST = 'buildserver.com'
      LATEST_BUILD_PATH = '/latest'

      def self.latest_build_message
        new(LATEST_BUILD_PATH).build_message
      end

      def initialize(path)
        @path = path
      end

      def build_message
        data['message']
      end

      private

      def data
        @data ||= JSON.parse(download_build)
      end

      def download_build
        Net::HTTP.get(HOST, @path)
      end
    end

## Connecting the Dots

With your unit test passing, you return to the integration test. At this point,
you no longer receive any errors about missing constants or undefined methods.
Instead, everything runs as you expect, but you're getting a different build
message: "All 126 tests passed." Where did that come from? As the gears start
turning, you realize that your test is fetching the actual build status.

There's no reason to make an actual HTTP request in the test, so you reach for
[WebMock](https://github.com/bblimke/webmock).

    # in spec/support/fake_continuous_integration.rb
    stub_request(:any, /buildserver.com/).to_rack(FakeContinuousIntegration)

Now any `Net::HTTP` requests to "buildserver.com" will route directly to your
fake, rather than actually opening a request. All that's left is to flesh out
our fake a little more:

    require 'sinatra/base'

    class FakeContinousIntegration < Sinatra::Base
      def self.stub_build_message(message)
        @@build_message = message
      end

      get '/latest' do
        content_type :json
        { 'message' => @@build_message }.to_json
      end
    end

Tests pass, page looks good. Time to ship.

## Two Words: Java. Script

It doesn't take long before somebody decides that it's not a good idea to query your build server in the middle of a request. Luckily, you realize that your build server comes fully equipped with a JSONP API, so you can offload that request to the browser:

    // in app/assets/javascripts
    function fetchBuildMessage(target) {
      $.ajax({
        url: 'http://buildserver.com/latest',
        dataType: 'jsonp',
        success: function(response) {
          $(target).text(response.message);
        }
      });
    }

    // in your .erb view
    fetchBuildMessage('#buildMessage');

Of course, your fake doesn't implement this <abbr title="JavaScript Object Notation">JSON</abbr> endpoint, so you have to fix that:

    get '/latest' do
      callback = params[:callback]
      data = { 'message' => @@build_message }.to_json
      "#{callback}(#{data})"
    end

You tag the scenario as `javascript` and let Capybara do its magic, but even after fixing your fake, it's regressed back to hitting the actual build server over HTTP. Testing this HTTP service was bad enough, and many developers shy away from testing their JavaScript, but the combination of the two is a formidable opponent. After coming this far, though, you're ready to do what it takes.

## What The World Needs Now Is Threads, More Threads

Tools like WebMock are great, but when testing JavaScript, it's a seperate browser process that loads the page, and not your Ruby test process. That means that the request to your build server isn't going through `Net::HTTP`; the requests are coming from Firefox or `capybara-webkit`, and those tools are gleefully unaware of your feeble attempts to reroute HTTP traffic. Fortunately, there are only two steps remaining towards the testing Holy Grail:

* The JavaScript is going to make an actual HTTP connection, so we need to have
  an actual HTTP server running somewhere with our fake.
* The JavaScript is talking to "buildserver.com," which we don't control, so we
  need to get it to use a configurable host.

We can use Capybara to solve the first issue. Instead of mounting the application using WebMock, we run it using `Capybara::Server`:

    class FakeContinousIntegration < Sinatra::Base
      def self.boot
        instance = new
        Capybara::Server.new(instance).tap { |server| server.boot }
      end
      # ...
    end

Next, we can put the CI host name in a constant. In most environments, this
will be "buildserver.com", but in the test environment, we can get the URL from
the server we just spun up:

    # config/environments/{development,staging,production}.rb
    CI_HOST = 'buildserver.com'

    # in spec/support/fake_continuous_integration.rb
    server = FakeContinuousIntegration.boot
    CI_HOST = [server.host, server.port].join(':')

Now we just need a parameter in our JavaScript function:

    // in app/assets/javascripts
    function fetchBuildMessage(host, target) {
      $.ajax({
        url: 'http://' + host + '/latest',
        dataType: 'jsonp',
        success: function(response) {
          $(target).text(response.message);
        }
      });
    }

    // in your .erb view
    fetchBuildMessage('<%= CI_HOST %>', '#buildMessage');

Made it, ma! Top of the world!

![Success](http://media.tumblr.com/tumblr_mcrxycBEvV1qzocnw.jpg)
