Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

Expanding from my previous post on a Grape API mounted on RACK.

Refactoring the Application Instance

Instead of sticking all of the Rack application code into config.ru, lets build a cleaner Acme::App _(in app/acme_app.rb). We’re going to drop _Rack::TryStatic and build this logic ourselves, since we might need to deal with other error codes than 404 (depending on your URL strategy you may be tripping over a 405). The logic remains the same: we try a bunch of static files and delegate to the API otherwise. You can also build primitive routing instead, so that everything requesting /api goes to the API and everything else goes to Rack::Static. Your mileage will vary.

module Acme
  class App
    def initialize
      @filenames = ['', '.html', 'index.html', '/index.html']
      @rack_static = ::Rack::Static.new(
        lambda { [404, {}, []] }, {
          :root => File.expand_path('../../public', __FILE__),
          :urls => %w[/]
        })
    end

    def call(env)
      request_path = env['PATH_INFO']
      # static files
      @filenames.each do |path|
        response = @rack_static.call(env.merge({'PATH_INFO' => request_path + path}))
        return response if response[0] != 404
      end
      # api
      Acme::API.call(env)
    end
  end
end

RSpec API Tests

Now that we have an application class, we can add API and Capybara integration tests. We start with RSpec and Rack test gems in Gemfile.

group :test do
  gem "rspec"
  gem "rack-test"
  gem "rspec-core"
  gem "rspec-expectations"
  gem "rspec-mocks"
end

The spec/spec_helper.rb adds Rack::Test.

require 'rubygems'

ENV["RACK_ENV"] ||= 'test'

require 'rack/test'
require File.expand_path("../../config/environment", __FILE__)

RSpec.configure do |config|
  config.mock_with :rspec
  config.expect_with :rspec
end

Testing an API involves making requests on the Rack application, pretty straightforward.

require 'spec_helper'

describe Acme::API do
  include Rack::Test::Methods

  def app
    Acme::API
  end

  context "v1" do
    context "system" do
      it "ping" do
        get "/api/v1/system/ping"
        last_response.body.should == { :ping => "pong" }.to_json
      end
    end
  end
end

RSpec Capybara Integration Tests

Notice that in the tests above we’re mounting the Rack application and making requests directly to it. Does it actually work in a browser? Do we see the public/index.html page?

We start by adding capybara into Gemfile. At the time of the writing we need to use the code from Capybara head, since it adds support for Capybara.app.

group :test do
  gem "capybara", :git => "https://github.com/jnicklas/capybara.git"
end

The spec/spec_helper.rb requires capybara/rspec, which brings in methods like page.visit and assigns an instance of the application to Capybara.app. Capybara will launch the application for us.

require 'capybara/rspec'

Capybara.configure do |config|
  config.app = Acme::App.new
end

An integration test can go into spec/integration and must be marked with request: true _and _js: true (the latter forces the use of the Selenium driver that will popup a browser). Let’s look for a proper title on the homepage.

require 'spec_helper'

describe "Grape on RACK", :js => true, :type => :request do
  context "homepage" do
    before :each do
      visit "/"
    end
    it "displays index.html page" do
      page.find("title").text.should == "Rack Powers Web APIs"
    end
  end
end

A POST, PUT and Some JQuery

The sample source in https://github.com/dblock/grape-on-rack also adds JQuery, extends the API to simulate a persisted counter, and makes PUT requests to it. Complete with an integration test. Run bundle install and bundle exec rackup _to see it and _bundle exec rspec spec to run the tests.

Backbone.js w/ Mongo

@knewter put together a neat Backbone.js + MongoDB w/ Mongoid demo using Grape that is built in a similar manner, https://github.com/knewter/grape_demo.