Breaking the Kubernetes Kill Chain: Host Path Mount
RSpec User Stories
1. RSpec and User Stories
A step by step tutorial
By Rahoul Baruah
http://www.brightbox.co.uk
Released under the Creative Commons Attribution Share-Alike Licence
2. 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.
3. 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.
4. 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
5. 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
6. 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”
7. 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
8. 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
9. registration and session steps
Given /^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
10. 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.
11. describe RegistrationsController do
describe quot;GET newquot; do
it quot;should show the form to allow someone to registerquot; do
on_getting :new do
expect_to_create_a_blank_user
end
response.should be_success
response.should render_template('registrations/new')
end
describe quot;POST createquot; do
it quot;should create and log in a new userquot; do
on_posting_to :create, :user => { quot;somequot; => :values } do
expect_to_build_a_new_user_with quot;somequot; => :values
expect_to_save_the_new_user_successfully
end
controller.current_user.should == @user
end
it quot;should redirect to the users dashboardquot; do
on_posting_to :create, :user => { quot;somequot; => :values } do
expect_to_build_a_new_user_with quot;somequot; => :values
expect_to_save_the_new_user_successfully
end
12. Cont’d…
it quot;should fail to create a new user if given invalid valuesquot; do
on_posting_to :create, :user => { quot;somequot; => :values } do
expect_to_build_a_new_user_with quot;somequot; => :values
expect_to_fail_to_save_the_new_user
end
controller.current_user.should be_blank
end
it quot;should reshow the registration form if given invalid valuesquot; do
on_posting_to :create, :user => { quot;somequot; => :values } do
expect_to_build_a_new_user_with quot;somequot; => :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
end
end
13. 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 quot;somequot; => :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.
14. 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
15. 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
16. Implementing the Controller
class 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
end
end
The implementation is pretty simple, which is exactly as it
should be.
17. 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.
18. 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
19. Specifying the Model
First attempt…
describe User do
it quot;should have a usernamequot; do
@user = User.new :username => ''
@user.should_not be_valid
@user.should have(1).errors_on(:username)
end
it quot;should have a unique usernamequot; 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…
20. Cont’d…
it quot;should confirm the password before savingquot; do
@user = User.new :password => 'secret', :password_confirmation => 'notsecret'
@user.should_not be_valid
@user.should have(1).errors_on(:password)
end
it quot;should encrypt the password before saving to the databasequot; 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'
end
end
21. 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
22. 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)
23. describe User do
it quot;should have a usernamequot; do
@user = a User, :username => ''
@user.should_not be_valid
@user.should have(1).errors_on(:username)
end
it quot;should have a unique usernamequot; 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 quot;should confirm the password before savingquot; do
@user = a User, :password => 'secret', :password_confirmation => 'notsecret'
@user.should_not be_valid
@user.should have(1).errors_on(:password)
end
24. 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.
25. 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.
26. 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)