How to use RSpec to describe a Sinatra application
Updated 2009-08-09 (originally posted ) — Ruby — 3 min read
This information was written a long time ago and has become pretty outdated.
Sinatra is a fun little web application microframework. Recently I started working on an application using Sinatra - and since I am working on good programming habits, before I dove into any coding I sat down to work out how to write specs for a Sinatra application.
Sinatra comes bundled with support for test/spec: a spec framework that builds on top of Rail's own Test::Unit to provide support for writing specs. Which is a really neat idea. But I have been using RSpec for my other work, and I wanted to continue doing so.
It turns out that RSpec takes a little bit of manual work to get going with Sinatra. I read a helpful article on gittr.com that pointed me in the right direction. The article advised me to add these lines to my spec files:
The first line loads your application; the second line loads RSpec; the third loads an RSpec-Test::Unit compatibility layer; and the fourth loads Sinatra's test helpers, which are written for Test::Unit.
Contrary to Sinatra's instructions for writing tests, you want to avoid loading 'sinatra/test/spec', which defines Sinatra's helper methods for test/spec, because that would load test/spec itself which conflicts with RSpec.
Those instructions mostly worked. I could write and run specs. But I had trouble with matchers. For example, this example:
Would give an error message like this:
Which I'm sure you can imagine is pretty annoying.
It was easy enough to identify 'sinatra/test/unit' as the root of the problem. When I removed that line RSpec's matchers worked fine; but then I didn't get Sinatra's test helpers, which make spec-writing much easier. So that wasn't a great solution either.
Examining Sinatra's code, I found that all 'sinatra/test/unit' does is to load 'sinatra/test/methods' - the actual helper methods - and mixes them into Test::Unit::TestCase. So I bypassed 'sinatra/test/unit' by copying and adapting some code from it to make the top of my spec file look like this:
Since that is a fair amount of setup, I moved all of it into a separate file called spec_helper.rb, which I loaded into my actual spec files. Because I am weird enough to write a Sinatra application that is split into multiple files and multiple spec files.
Anyway, now my specs run just as they should, with Sinatra's helpers and everything:
The next challenge was to write specs for Sinatra helpers. Although Sinatra actions and helpers generally appear in the outermost namespace, the DSL methods that define them actually bind the helpers to Sinatra::EventContext. You can't invoke helper methods directly from an example context; you have to create an instance of Sinatra::EventContext and send the method call to that - much the same way Rails instantiates a subclass of ActionController::Base to handle a controller action. Here is the code you will want in your example groups:
body= method has to be defined on the response mock to prevent an
error. But it doesn't have to actually do anything. With that setup
code in place, you can do this:
and write something like this in your application:
And now you have a reasonably complete speccing setup. There are still
a couple of issues though. For one thing, the spec helpers are missing
a much needed
assigns method. As it stands there is no good way to
pry apart the behavior of an action if there is no convenient method
call to stub. You can only define the parameters that are passed to
it, and read response. On the upside, this does help to enforce good
The other issue is more of an annoyance than a serious problem. It
seems that somewhere in all of this there are one or two
method_missing definitions that bounce calls back and forth. If call
a method that is not defined, you generally won't get an "undefined
method" error, you will get a "stack level too deep" error
instead. This is particularly unhelpful because it does not tell you
what class received the undefined method, or which method is
undefined. So a little extra manual stack tracing is required when