
Testing an application that integrates with an HTTP service can be tricky:
It may seem like a world of pain, but you’re not going to let a few HTTP requests get between you and your TDD, are you?
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.
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 TDD 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
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
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.
# 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.
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 JSON 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.
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:
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!

Fast tests mean a fast feedback loop when doing TDD. Let’s make our tests fast with Guard.
A Jasmine spec in CoffeeScript:
describe 'A suite', ->
it 'contains spec with an expectation', ->
expect(true).toBe(true)
Gemfile:
source 'https://rubygems.org'
gem 'jasmine'
gem 'guard'
gem 'guard-coffeescript'
gem 'guard-livereload'
Guardfile:
guard :coffeescript, output: 'javascripts' do
watch(%r{^src/(.*)\.coffee})
end
guard :coffeescript, output: 'spec/javascripts' do
watch(%r{^spec/src/(.*)\.coffee})
end
guard 'livereload' do
watch(%r{^spec/javascripts/.*/(.*)\.js})
watch(%r{^spec/javascripts/(.*)\.js})
watch(%r{^javascripts/.*/(.*)\.js})
watch(%r{^javascripts/(.*)\.js})
end
This compiles CoffeeScript, watches our specs or Javascript files for changes, and reloads our browser automatically.
Start a jasmine server:
rake jasmine
Visit localhost:8888. See the results of the specs.
Change the specs or Javascript files and the browser will automatically reload.
Constants help us avoid Magic Numbers and repeated code, which violates the DRY principle.
Recently, I needed to count down the the remaining characters in a text field.
I limited the length of the attribute using a constant:
class Event < ActiveRecord::Base
NAME_MAX_LENGTH = 70
validates :name, length: { maximum: NAME_MAX_LENGTH }
# ...
end
Then I repeated myself in the JavaScript:
# app/assets/javascripts/countdown_event_name.js
var max = 70
“This is a nasty scenario - I’ve
got the same value in two places!”, says I. How else could I
access the constant more cleanly from my Event model in the JavaScript?
I discovered you can use ERB in JavaScript if you change file extensions and trigger preprocessing like so:
# Rename countdown_event_name.js → countdown_event_name.js.erb
var max = <%= Event::NAME_MAX_LENGTH %>
Appending .js.erb to the filename initiates asset pipeline preprocessing. Processing occurs in order, right-to-left, so make sure to keep things in order. For example, .js.coffee.erb is processed as ERB first, then processed as CoffeeScript, then served as a JavaScript file for the browser to interpret.
In episode #5 of the Giant Robots Smashing into other Giant Robots podcast, Ben Orenstein is joined by “Cowboy” Ben Alman, JavaScript open source developer and Director of Pluginization at Bocoup.
They discuss how Ben Alman got started with programming and his crazy projects, the open web, favorite tools, and one of his latest projects, Grunt. They also discuss CoffeeScript, and why Ben Alman’s not using it yet, speaking at conferences and how Ben Alman got more comfortable in front of crowds, whether its important to understand straight JavaScript, or just jQuery, and more! Also, Whiskey.
Call us toll-free at 1-877-9-ROBOTS x198 and leave a voicemail. That’s (877) 976-2687 x198, email your questions to info@thoughtbot.com or Tweet to us @thoughtbot.
Follow @thoughtbot, @cowboy, and @r00k on twitter.
Last Friday, October 14th, we hosted a two hour live Q&A chat for purchasers of our Backbone.js on Rails eBook with co-author Jason Morrison. We invited people who have downloaded the book to submit questions ahead of time, and then join us in chat for a discussion.
We’re pleased to offer the recording and notes as a free resources. You can download the Backbone.js on Rails live Q&A chat audio and notes over on our Workshops site.