Testing a Rack app

Let’s grab our very Rack application from the book Webapps for Beginners.

It looked like this:

class Application
  def call(env)
    handle_request(env["REQUEST_METHOD"], env["PATH_INFO"])
  end

  private

    def handle_request(method, path)
      if method == "GET"
        get(path)
      else
        method_not_allowed(method)
      end
    end

    def get(path)
      [200, { "Content-Type" => "text/html" }, ["You have requested the path #{path}, using GET"]]
    end

    def method_not_allowed(method)
      [405, {}, ["Method not allowed: #{method}"]]
    end
end

How do we test such an app?

We could do this manually in RSpec like so:

describe Application do
  context "get to /ruby/monstas" do
    it "returns the body" do
      env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" }
      response = app.call(env)
      body = response[2][0]
      expect(body).to eq "You have requested the path /ruby/monstas, using GET"
    end
  end
end

Or we could make some of this a little more reusable, like so:

describe Application do
  context "get to /ruby/monstas" do
    let(:app)      { Application.new }
    let(:env)      { { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" } }
    let(:response) { app.call(env) }
    let(:body)     { response[2][0] }

    it "returns the body" do
      expect(body).to eq "You have requested the path /ruby/monstas, using GET"
    end
  end
end

And add a test for the status code:

describe Application do
  context "get to /ruby/monstas" do
    let(:app)      { Application.new }
    let(:env)      { { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" } }
    let(:response) { app.call(env) }
    let(:status)   { response[0] }
    let(:body)     { response[2][0] }

    it "returns the status 200" do
      expect(status).to eq 200
    end

    it "returns the body" do
      expect(body).to eq "You have requested the path /ruby/monstas, using GET"
    end
  end
end

This passes:

$ rspec rack_spec.rb
..

Finished in 0.00086 seconds (files took 0.1486 seconds to load)
2 examples, 0 failures

Our Rack application is just a Ruby class which, when started (e.g. with rackup), will be hooked up to the web server, and called whenever an HTTP request comes in (e.g. from the browser).

But we can also just instantiate it ourselves, and call the method call with a hash that complies to the Rack env conventions. E.g. in our case we’d want to set the REQUEST_METHOD and PATH_INFO keys.

While this works well, it’s also a little bit of a hassle. And that’s where Rack::Test can help.

Here’s how we can use Rack::Test to make our tests a little less verbose:

require "rack/test"

describe Application do
  include Rack::Test::Methods

  context "get to /ruby/monstas" do
    let(:app) { Application.new }

    it "returns the status 200" do
      get "/ruby/monstas"
      expect(last_response.status).to eq 200
    end

    it "returns the body" do
      get "/ruby/monstas"
      expect(last_response.body).to eq "You have requested the path /ruby/monstas, using GET"
    end
  end
end

Rack::Test’s helper methods expect that there’s a method app defined, so they can call it. We can implement that using RSpec’s handy let feature.

Now we first call the Rack::Test method get with our path. This method creates the env hash for us, and calls call on the application. So we don’t have to compose the nasty hash ourselves.

After this, the method last_response will return the response that our request has returned, and we can test it.

But the method get also returns the same response. So we could make this a little more concise, and remove the duplicate call get "/ruby/monstas" like this:

require "rack/test"

describe Application do
  include Rack::Test::Methods

  context "get to /ruby/monstas" do
    let(:app)      { Application.new }
    let(:response) { get "/ruby/monstas" }

    it { expect(response.status).to eq 200 }
    it { expect(response.body).to include "/ruby/monstas, using GET" }
  end
end

We can also remove the include line from our actual tests, and move it to the RSpec configuration. This configuration normally would sit in a separate file called spec_helper.rb, but for now we’ll just move it above our test code:

require "rack/test"

RSpec.configure do |config|
  config.include Rack::Test::Methods
end

describe Application do
  context "get to /ruby/monstas" do
    let(:app)      { Application.new }
    let(:response) { get "/ruby/monstas" }

    it { expect(response.status).to eq 200 }
    it { expect(response.body).to include "/ruby/monstas, using GET" }
  end
end

Nice.

Let’s add another test for the case when an unsupported HTTP method is used:

describe Application do
  let(:app) { Application.new }

  context "get to /ruby/monstas" do
    let(:response) { get "/ruby/monstas" }
    it { expect(response.status).to eq 200 }
    it { expect(response.body).to include "/ruby/monstas, using GET" }
  end

  context "post to /" do
    let(:response) { post "/" }
    it { expect(response.status).to eq 405 }
    it { expect(response.body).to eq "Method not allowed: POST" }
  end
end

As you see we’ve moved the let(:app) statement one level up so it can be shared among both contexts. the let(:response) statement on the other hand is different for both contexts, so we kept them there.

Again, this passes:

$ rspec rack_spec.rb
....

Finished in 0.01175 seconds (files took 0.21704 seconds to load)
4 examples, 0 failures

Very cool. We’ve used RSpec and Rack::Test to write a few tests for the functionality in our first Rack application.

Let’s head over to the next chapter and do the same for our Sinatra resource from the Webapps for Beginners book. We’ll see a few more Rack::Test helper methods there.