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).
MockRequest
passing the app
instance in to the constructor#get
on the mock request object to simulate a GET request to our app
200
, it has a Content-Type
header of text/html
, and that the body is '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:
Let’s see how we can mock a POST request.
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:
Rack::Request
passing in envreq.params
to get a value from the POST dataOnce 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.
Finally, let’s see how to expect that the App
returns a request with a Cookie in, and how to actually return the Cookie.
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:
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:
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:
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.