RSpec plain text story runner on a fresh rails app

Posted on January 25, 2008

RSpec is a wonderful Behaviour Driven Development framework that is also very suitable for Rails applications. After giving a talk on it for the Scotland Ruby usergroup I found that people are most impressed with its plain text story runner feature. I have found documentation on this rather meager so I created this post on how to setup a rails app from scratch that has a story with some scenarios that all pass. Once I had this in place the rest was a piece of cake*...

To find out more about how the story runner works have a look at the links at the bottom of this post. The peepcode on the issue is good and between the posts by David Chelimsky - the creator of this stuff - there is some useful information as well.

The central idea is to define each part of an acceptance test (given ... when ... then ...) as an English sentence. These sentences are then defined as 'steps' which perform the corresponding action or test. Once you have built up a decent library of steps a client can have a good shot at writing his own acceptance tests. Any sentences that are not yet covered by a step will show up as pending.

On with the practical business!

We do not need much of a rails app for us to be able to test it with a story. I will just use a fresh rails install with the needed RSpec and restful_authentication plugins. Like so:

$ rails storyapp
$ cd storyapp
$ rake db:create:all # after setting up database.yml for your system
$ ./script/plugin install git://github.com/dchelimsky/rspec.git
$ ./script/plugin install git://github.com/dchelimsky/rspec-rails.git
$ ./script/generate rspec
$ ./script/plugin install git://github.com/technoweenie/restful-authentication.git
$ ./script/generate authenticated user
$ rake db:migrate
$ rake db:test:prepare
Add the following snippet to your config/routes.db to define login/logout routes:
map.with_options :controller => "sessions" do | page |
   page.login "/login", :action => "new"
   page.logout "/logout", :action => "destroy"
end

At this stage you should be able to start your rails server and find a login form at http://localhost:3000/login. Our application behaves as follows: if you login with invalid credentials (all of them are until a user is created) you will be shown the login form again. If you create a user from http://localhost:3000/users/new this user will be created in the database and you will be redirected to "/" (for now still showing the default rails page). So let's write a story that tests this behaviour. First there is some more setting up to do though...

I am choosing the convention to name my text stories stories/foo_story and the corresponding runner stories/foo_story.rb. All steps will be named stories/steps/foo_steps.rb. My spec_helper will automatically require all the step files in the stories/steps directory. This is how we set this up:

$ mkdir stories/steps
and add stuff to your stories/helper.rb so that it looks like this:
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'spec/rails/story_adapter'
require 'ruby-debug'

dir = File.dirname(__FILE__)
Dir[File.expand_path("#{dir}/steps/*.rb")].uniq.each do |file|
  require file
end

##
# Run a story file relative to the stories directory.

def run_local_story(filename, options={})
  run File.join(File.dirname(__FILE__), filename), options
end

This is the story we will be using, so go ahead and save that as stories/login_story:

Story: User logging in

  As a user
  I want to login with my details
  So that I can get access to the site

  Scenario: User uses wrong password

    Given a username 'jdoe'
    And a password 'letmein'

    When the user logs in with username and password

    Then the login form should be shown again

  Scenario: Creates a new user

    Given a username 'jdoe'
    And a password 'letmein'
    And an email 'jdoe@test.com'
    And there is no user with this username

    When the user creates an account with username, password and email

    Then there should be a user named 'jdoe'
    And should redirect to '/'

And this is the corresponding stories/login_story.rb which just runs the file we just created using the steps we are about to define:

require File.dirname(__FILE__) + "/helper"

with_steps_for(:login) do
  run_local_story "login_story", :type => RailsStory
end

Save the steps for this story to stories/steps/login_steps.rb:

steps_for(:login) do
  Given "a username '$username'" do |username|
    @username = username
  end

  Given "a password '$password'" do |password|
    @password = password
  end

  Given "an email '$email'" do |email|
    @email = email
  end

  Given "there is no user with this username" do
    User.find_by_login(@username).should be_nil
  end

  When "the user logs in with username and password" do
    post "/sessions/create", :user => { :login => @username, :password => @password }
  end

  When "the user creates an account with username, password and email" do
    post "/users/create", :user => { :login => @username,
                                     :password => @password,
                                     :password_confirmation => @password,
                                     :email => @email }
  end

  Then "the login form should be shown again" do
    response.should render_template("sessions/new")
  end

  Then "there should be a user named '$username'" do |username|
    User.find_by_login(username).should_not be_nil
  end

  Then "should redirect to '$path'" do |path|
    response.should redirect_to(path)
  end
end

When you put a $something in the sentence of a step, RSpec will extract a value from a matching sentence in your story and pass that into the block. Note that what you put after your $ is just fluff. It is good practice to put quotes around your values so that there are less cases where the regexp matcher will get confused and split things up in a way you did not expect. Some character based regexp expressions are allowed as well. So Then "it has $n users?" will match "then it has 2 users" as well as "then it has 1 user".

We're all set! Run:

$ ruby stories/login_story.rb
et voilà:
2 scenarios: 2 succeeded, 0 failed, 0 pending

Success!

Here is a tarball with the git repos of the steps taken in this article.

Additional info on RSpec's plain text story runner:

*: the cake is a lie!

Update 2008-05-27: I have changed the plugin install commands to use the git repositories instead. The original install commands were:

$ ./script/plugin install svn://rubyforge.org/var/svn/rspec/trunk/rspec
$ ./script/plugin install svn://rubyforge.org/var/svn/rspec/trunk/rspec_on_rails
$ ./script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/