Home Contact GitHub
TDDing HTTP Requests in a plain Rack app

In this post I want to outline how to TDD a simple backend web application using Rack , a very minimal Ruby interface for creating a server side application that can respond to HTTP requests. Rack is actually really awesome for simple web app projects because of it being so minimal — you don’t need to worry about learning how to use routes or decorators or all the settings that typically come with a backend web framework.

With Rack, we simply implement a Class that can respond to the method #call(env), returning an array [response_code, headers, body]. The response code must be an integer, the headers a hash, and the body something that can respond to each — in the basic case, we can just use a string wrapped in array.

When Rack receives an HTTP request, it sends the #call message to the App instance that we set it up with, passing in a parameter called env. env is a hash that contains information about the request which was received by Rack. Your call method can do whatever you want it to do (using env to access information from the request) as long as you return an array at the end conforming to Rack’s expectations. Rack will respond to the HTTP request based on the contents of your returned array.

First let’s look at how to TDD a response to a basic GET request. We’ll use the built-in MockRequest class which allows us to mock HTTP requests to our app without actually making them. We’ll use TDD to return the responses we want. (Note that in reality this would be done in much smaller steps rather than all at once).

  1. instantiate MockRequest passing the app instance in to the constructor
  2. call #get on the mock request object to simulate a GET request to our app
  3. expect that the response status is 200, it has a Content-Type header of text/html, and that the body is 'Hello World'
app = App.new
mock_request = Rack::MockRequest.new(app)

response = mock_request.get('')

expect(response.status).to eq(200)
expect(response.get_header('Content-Type')).to eq('text/html')
expect(response.body).to eq('Hello World')

When you call #get/#post/etc. on a MockRequest instance, it returns a MockResponse instance, which has some useful methods for testing the response that was returned. NB: the '' passed in to the call to #get is the path for the request.

Apart from #status and #body, #get_header is very useful for checking if a particular header was present in the response. (Check out MockRequest and MockResponse to see the other methods they offer).

So here’s how we would write our App class such that it passes the above expectations:

class App
  def call(env)
    [200, {'Content-Type' => 'text/html'}, ['Hello World']]
  end
end

Let’s see how we can mock a POST request.

response = mock_request.post('', :input => "move=8")

In order to pass data along with the POST request, we pass a hash with an :input key as a parameter to #post.

Inside our app, we can access the data content of the POST request by using Rack’s Request class. The Request class is instantiated with the env passed into the app by Rack. We can think of Rack::Request as a class that fleshes the env out with a lot of convenience methods. So here’s how to actually do it:

  1. instantiate Rack::Request passing in env
  2. index into req.params to get a value from the POST data
class App
  def call(env)
    req = Rack::Request.new(env)
    req.params['move']
    ...
  end
end

Once you’ve instantiated Rack::Request, you can access a whole lot of other convenience methods, including: req.post? to check if the request had the method POST (you can use this to give your app different behaviour for GETs vs POSTs), req.path_info to check what path the request was made to, req.cookies to access cookies directly, req.body to get the body of the request, req.get_header, and many others.

Cookies

Finally, let’s see how to expect that the App returns a request with a Cookie in, and how to actually return the Cookie.

expect(response.get_header('Set-Cookie')).to eq('session_id=1')

Cookies are stored in the Set-Cookie header in the HTTP response, so you just need to expect that this header contains the Cookies you want to be set by the App. (In the test assertion above, we expect the cookie session_id=1 to have been set).

We can make the test pass by simply setting the Set-Cookie header on the response returned by our App class:

class App
  def call(env)
    ...
    [200, {'Set-Cookie' => 'session_id=1'}, ['Hello World']]
  end
end

When you’ve sent a response with a cookie, your browser will then include that cookie in any further requests it makes.

If we want to test that our App behaves differently when it receives a request with a cookie already set, we can mock this behaviour like so:

response = mock_request.post('', 'HTTP_COOKIE' => 'session_id=3')

This will make a mock POST request that looks like the browser previously received the cookie session_id=3 from the server.

Finally, I just want to note that I haven’t shown any use of Rack’s Response class, which can be used in the App class to build responses more conveniently. If we had used the Response class, we could have set a Cookie in our response using the convenience methods it provides:

class App
  def call(env)
    ...
    response = Rack::Response.new(
      ['Hello World'],
      200,
      {'Content-Type' => 'text/html'},
    )
    response.set_cookie('session_id', '3')
    response
  end
end

When instantiating a Response object, you pass in your body iterable, then your status integer, and finally your headers hash.

Rack::Response is useful when you want to do something more complicated than just setting a single Cookie in your response.