RSpec User Stories

Preview:

DESCRIPTION

A step-by-step tutorial outlining the use of RSpec and Cucumber to develop applications with automated acceptance tests

Citation preview

RSpec and User StoriesA step by step tutorial

By Rahoul Baruahhttp://www.brightbox.co.uk

Released under the Creative Commons Attribution Share-Alike Licence

What is RSpec? Behaviour Driven Development An evolution of Test Driven Development Concentrates on the “behaviour” of your

system Specify the behaviour first, implement it

second, refactor it third.

What are User Stories? Acceptance Tests for RSpec Describe the functionality of your application

in terms your customer will understand Prove that the application does what it is

supposed to.

A Simple Authentication System Write the feature Write verification code for the feature Specify the controller Implement the controller Specify the models Implement the models Verify the feature works as required Refactor

Write our Feature Write our feature and save it as features/

allow-a-user-to-login.feature Show it to the customer and have it approved Run rake features

Feature: Allow a User to log in As a user, I want to log in, So I can see my stuff Scenario: Successful login

Given a user called Dave with a password of secret And 15 items of stuff When I log in as Dave with password secret Then I should see the Dashboard page And it should list my 15 items of stuff

Scenario: Unsuccessful login Given a user called Dave with a password of secret When I log in as Dave with password potato Then I should see the Login page And I should see a message saying “an incorrect username or

password was supplied”

rake features When we run ‘rake features’ it tells us that

none of the features are implemented So we start with the first step and implement

that

Steps - proving a feature works Create Ruby files, registration-steps.rb and

session-steps.rb, in features/steps These contain the code verifying that the

feature works as expected

registration and session stepsGiven /^a user called (.*) with a password of (.*)$/ do | username, password |

user = User.find_by_username username

user.destroy unless user.nil?

visits '/registrations/new'

fills_in 'User name', :with => username

fills_in 'Password', :with => password

fills_in 'Password Confirmation', :with => password

clicks_button 'Register'

end

When /^I log in as (.*) with password (.*)$/ do | username, password |

visits '/sessions/new'

fills_in 'User name', :with => username

fills_in 'Password', :with => password

clicks_button 'Log in'

end

Then /^I should see the Dashboard page$/ do

response.should redirect_to('/dashboard')

end

Use Webrat to define our interactions with the application Use RSpec to check the responses from the application

Specify our Controller ‘rake features’ fails So we need to start writing some code to make it pass But before we write any code, we need a specification So we run… ruby script/generate rspec_controller Registrations

…to build a blank controller and specification Now to implement the RegistrationsController. Similar work needs to be done with the

SessionsController for actually logging in.

describe RegistrationsController do describe "GET new" do it "should show the form to allow someone to register" do on_getting :new do expect_to_create_a_blank_user end response.should be_success response.should render_template('registrations/new') end

describe "POST create" do it "should create and log in a new user" do on_posting_to :create, :user => { "some" => :values } do expect_to_build_a_new_user_with "some" => :values expect_to_save_the_new_user_successfully end controller.current_user.should == @user end it "should redirect to the users dashboard" do on_posting_to :create, :user => { "some" => :values } do expect_to_build_a_new_user_with "some" => :values expect_to_save_the_new_user_successfully end

Cont’d…

it "should fail to create a new user if given invalid values" do on_posting_to :create, :user => { "some" => :values } do expect_to_build_a_new_user_with "some" => :values expect_to_fail_to_save_the_new_user end controller.current_user.should be_blank end

it "should reshow the registration form if given invalid values" do on_posting_to :create, :user => { "some" => :values } do expect_to_build_a_new_user_with "some" => :values expect_to_fail_to_save_the_new_user end response.should render_template('/sessions/new') flash[:error].should == 'There were some errors when registering your details' end endend

Specification for new registrations

Use helper methods to make the specification easier to read Use mock objects so we are testing just the controller, not the

(non-existent) models Note how we can supply "some" => :values for

the :registration parameter. As we are not using real models, the actual fields do not matter; what counts is how the controller behaves in response to certain actions.

def expect_to_create_a_blank_user @user = mock_model User User.should_receive(:new).and_return(@user)end

def expect_to_build_a_new_user_with parameters @user = mock_model User User.should_receive(:new).with(parameters).and_return(@user)end

def expect_to_save_the_new_user_successfully @user.should_receive(:save!).and_return(true)end

def expect_to_fail_to_save_the_new_user prepare_for_errors_on @user @user.should_receive(:save!).and_raise(ActiveRecord::RecordInvalid.new(@user))end

The helpers build a mock user We tell the mock to expect certain method calls and return the

correct responses

Implement the Controller We do need to build a model… ruby script/generate rspec_model User …so that our steps will run Remove the < ActiveRecord::Base from the definition for now

(so that ActiveRecord does not complain about our lack of database tables)

But we don’t implement anything in it yet; our controller specification is using mocks so does not actually need a real object

We also need to add some routes to get things moving… map.resources :registrations map.resources :sessions map.resource :dashboard

Implementing the Controllerclass RegistrationsController < ApplicationController def new @user = User.new end def create @user = User.new params[:user] @user.save! redirect_to dashboard_path rescue ActiveRecord::RecordInvalid flash[:error] = 'There were some errors when registering your details' render :template => 'registrations/new', :status => 422 endend

The implementation is pretty simple, which is exactly as it should be.

Specifying the Model Now we have our controller behaving as

specified we need to specify the behaviour of our User model

We have already generated the RSpec files, we just need to tell it how we expect a User to behave.

Specifying the Model We write the specs Update the migration to deal with the fields we

need We switch the model back so that it descends

from ActiveRecord::Base We run the migration We implement the model

Specifying the ModelFirst attempt…describe User do it "should have a username" do @user = User.new :username => '' @user.should_not be_valid @user.should have(1).errors_on(:username) end it "should have a unique username" do @first_user = User.create! :username => 'arthur', :password =>

'12345', :password_confirmation => '12345' @second_user = User.new :username => 'arthur' @second_user.should_not be_valid @second_user.should have(1).errors.on(:username) end

Cont’d…

Cont’d…

it "should confirm the password before saving" do @user = User.new :password => 'secret', :password_confirmation => 'notsecret' @user.should_not be_valid @user.should have(1).errors_on(:password) end it "should encrypt the password before saving to the database" do PasswordEncrypter.should_receive(:encrypt).with('12345').and_return('3ncrypt3d') @user = User.new :username => 'arthur', :password => '12345', :password_confirmation

=> '12345' @user.save! @user.encrypted_password.should == '3ncrypt3d' endend

Specifying the Model There are two areas of the specification that “smell

bad” Both “it should have a unique username” and “it should

encrypt the password before saving to the database” require a valid object to be saved; which in turn require knowledge of the object beyond that individual specification clause

Ideally this would not be necessary but is a problem with the ActiveRecord pattern; mixing business logic and persistence logic in a single class

The (imperfect) solution is to use object ‘factories’ that encapsulate knowledge of a valid model.

We are still spreading that knowledge outside of the specification but at least it is a single place to make that change (usually in spec/spec_helper.rb)

describe User do it "should have a username" do @user = a User, :username => '' @user.should_not be_valid @user.should have(1).errors_on(:username) end it "should have a unique username" do @first_user = a_saved User, :username => ‘arthur’ @second_user = a User, :username => 'arthur' @second_user.should_not be_valid @second_user.should have(1).errors.on(:username) end it "should confirm the password before saving" do @user = a User, :password => 'secret', :password_confirmation => 'notsecret' @user.should_not be_valid @user.should have(1).errors_on(:password) end

Rewritten to use “a Something” and “a_saved Something” as an object factory

Each specification clause only specifies the information it needs.

The factory ensures that the rest of the object is valid.

Acceptance By now we should have done enough to let the

first step in our story pass its acceptance test rake features will prove it That should be all we need to do for that

particular step - any extra development is unnecessary as it has not been requested by the customer

However, we can now safely refactor the code, to organise it better, safe in the knowledge that we can prove that it still does what is required without breakages.

Appendix We have a fork of RSpec for Rails that provides a set of helper

methods: prepare_for_errors_on, on_getting, on_posting_to, on_putting_to and on_deleting_from

See http://github.com/rahoulb/rspec-rails/wikis/home We are working on an object factory that makes building

models (without full knowledge of their internals) a bit simpler…

Object.factory.when_creating_a User, :auto_generate => :username, :auto_confirm => :password

@user = a User @user = a_saved User # as above but auto-saves the user @user.should be_valid

See http://github.com/rahoulb/object-factory/wikis (although at the time of writing, November 2008, this is not quite ready)

www.brightbox.co.ukhello@brightbox.co.uktwitter.com/brightbox