View
8.169
Download
3
Category
Tags:
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
Recommended