Rspec Documentation
RSPEC
RSpec is a great tool in the behavior driven design process of writing human readable specifications that direct and validate the development of your application. I've found the following practices helpful in writing elegant and maintainable specifications.First #describe What You Are Doing
Begin by using #describe for each of the methods you plan on defining, passing the method’s name as the argument. For class method specs prefix a "." to the name, and for instance level specs prefix a "#". This follows standard Ruby documentation practices and will read well when output by the spec runner.
describe User do describe '.authenticate' do end describe '.admins' do end describe '#admin?' do end describe '#name' do end endThen Establish The #context
Next use #context to explain the different scenarios in which the method could be executed. Each #context establishes the state of the world before executing the method. Write one for each execution path through a method.
For example, the following method has two execution paths:
class SessionsController < ApplicationController def create user = User.authenticate :email => params[:email], :password => params[:password] if user.present? session[:user_id] = user.id redirect_to root_path else flash.now[:notice] = 'Invalid email and/or password' render :new end end endThe spec for this method would consists of two contexts:
describe '#create' do context 'given valid credentials' do end context 'given invalid credentials' do end endNote the use of the word "given" in each #context. This communicates the context of receiving input. Another great word to use in a context for describing conditional driven behavior is "when".
describe '#destroy' do context 'when logged in' do end context 'when not logged in' do end endBy following this style, you can then nest #contexts to clearly define further execution paths.
And Finally Specify The Behavior
Strive to have each example specify only one behavior. This will increase the readability of your specs and make failures more obvious and easier to debug.
The following is a spec with multiple un-related behaviors in a single example:
describe UsersController do describe '#create' do ... it 'creates a new user' do User.count.should == @count + 1 flash[:notice].should be response.should redirect_to(user_path(assigns(:user))) end end endBreak out the expectations into separate examples for a more clear definition of the different behaviors.
describe UsersController do describe '#create' do ... it 'creates a new user' do User.count.should == @count + 1 end it 'sets a flash message' do flash[:notice].should be end it "redirects to the new user's profile" do response.should redirect_to(user_path(assigns(:user))) end end endTips For Better Examples
Lose The Should
Don't begin example names with the word "should". It is redundant and results in hard to read spec output. Instead write examples by starting with a present tense verb that describes the behavior.
it 'creates a new user' do end it 'sets a flash message' do end it 'redirects to the home page' do end it 'finds published posts' do end it 'enqueues a job' do end it 'raises an error' do endDon't hesitate to use words like "the" or "a" or "an" in your examples when they improve readability.
Use The Right Matcher
RSpec comes with a lot of useful matchers to help your specs read more like natural language. When you feel there is a cleaner way ... there usually is.
Here are some common matcher refactorings to help improve readability.
# before: double negative object.should_not be_nil # after: without the double negative object.should be # before: "lambda" is too low level lambda { model.save! }.should raise_error(ActiveRecord::RecordNotFound) # after: for a more natural expectation replace "lambda" and "should" with "expect" and "to" expect { model.save! }.to raise_error(ActiveRecord::RecordNotFound) # the negation is also available as "to_not" expect { model.save! }.to_not raise_error(ActiveRecord::RecordNotFound) # before: straight comparison collection.size.should == 4 # after: a higher level size expectation collection.should have(4).itemsPrefer Explicitness
#it, #its and #specify may cut down on the amount of typing but they sacrifice readability. Using these methods requires you to read the body of the example in order to determine what its specifying. Use these sparingly if at all.
Let's compare the output from the documentation formatter of the following spec that uses these more concise example methods.
describe PostsController do describe '#new' do context 'when not logged in' do ... subject do response end it do should redirect_to(sign_in_path) end its :body do should match(/sign in/i) end end end end $ rspec spec/controllers/posts_controller_spec.rb --format documentation PostsController #new when not logged in should redirect to "/sign_in" should match /sign in/iRunning this spec results in blunt, code-like output with redundancy from using the word "should" multiple times.
Here is the same spec using more verbose, explicit examples:
describe PostsController do describe '#new' do context 'when not logged in' do ... it 'redirects to the sign in page' do response.should redirect_to(sign_in_path) end it 'displays a message to sign in' do response.body.should match(/sign in/i) end end end end $ rspec spec/controllers/posts_controller_spec.rb --format documentation PostsController #new when not logged in redirects to the sign in page displays a message to sign inThis version results in a very clear, readable specification.
Run Specs To Confirm Readability
Always run your specs with the "--format" option set to "documentation" (in RSpec 1.x the --format options are "nested" and "specdoc")
$ rspec spec/controllers/users_controller_spec.rb --format documentation UsersController #create creates a new user sets a flash message redirects to the new user's profile #show finds the given user displays its profile #show.json returns the given user as JSON #destroy deletes the given user sets a flash message redirects to the home pageContinue to rename your examples until this output reads like clear conversation.
Formatting
Use "do..end" style multiline blocks for all blocks, even for one-line examples. Further improve readability and delineate behavior with a single blank line between all #describe blocks and at the beginning and end of the top level #describe.
Before:
describe PostsController do describe '#new' do context 'when not logged in' do ... subject { response } it { should redirect_to(sign_in_path) } its(:body) { should match(/sign in/i) } end end endAnd after:
describe PostsController do describe '#new' do context 'when not logged in' do ... it 'redirects to the sign in page' do response.should redirect_to(sign_in_path) end it 'displays a message to sign in' do response.body.should match(/sign in/i) end end end endA consistent formatting style is hard to achieve with a team of developers but the time saved from having to learn to visually parse each teammate's style makes it worthwhile.
Rspec with Rails-3
Install Rails-3$ gem install rails -v "~> 3.0.0"
Generate an app
$ rails new example $ cd example
Add rspec-rails to the Gemfile
$ echo 'gem "rspec-rails", :group => [:development, :test]' >> Gemfile
Install the bundle
$ bundle install
Bootstrap RSpec
$ rails generate rspec:install
Generate a scaffold
$ rails generate scaffold
Students
name:string branch:string marks:integer
$ rails generate scaffold
Subjects
student_id:integer name:string author:string
This generates files in theapp
andspec
directories. The files in theapp
directory are generated by Rails, and Rails delegates the generation of the files in thespec
directory to RSpec. add has_many :subjects in models/students.rb add belongs_to :student in models/subject.rb
Run migrations
$ rake db:migrate && rake db:test:prepare
Run RSpec
$ rake spec
or$ rspec spec --format documentation
If all went well, you should see output ending with:69 examples, 0 failures, 3 pending
This output also includes the following controller spec:SubjectsController GET index assigns all subjects as @subjects GET show assigns the requested subject as @subject GET new assigns a new subject as @subject GET edit assigns the requested subject as @subject POST create with valid params creates a new Subject assigns a newly created subject as @subject redirects to the created subject with invalid params assigns a newly created but unsaved subject as @subject re-renders the 'new' template PUT update with valid params updates the requested subject assigns the requested subject as @subject redirects to the subject with invalid params assigns the subject as @subject re-renders the 'edit' template DELETE destroy destroys the requested subject redirects to the subjects list StudentsController GET index assigns all students as @students GET show assigns the requested student as @student GET new assigns a new student as @student GET edit assigns the requested student as @student POST create with valid params creates a new Student assigns a newly created student as @student redirects to the created student with invalid params assigns a newly created but unsaved student as @student re-renders the 'new' template PUT update with valid params updates the requested student assigns the requested student as @student redirects to the student with invalid params assigns the student as @student re-renders the 'edit' template DELETE destroy destroys the requested student redirects to the students list
Output like this can help to quickly gain a high level understanding of how an object behaves. It also exposes which cases have been specified and which have not. Note the balance between the examples for the create
and update
actions. If the redirects to the widget
example was missing from one or the other, it would be easy to spot.Take a look at the generated
spec/controllers/widgets_controller_spec.rb
to get a sense of how to organize your specs to generate output like this.request spec
Request specs provide a thin wrapper around Rails' integration tests, and are designed to drive behavior through the full stack, including routing (provided by Rails) and without stubbing (that's up to you).
With request specs, you can:
RSpec provides two matchers that delegate to Rails assertions:
If you would like to use webrat or capybara with your request specs, all you have to do is include one of them in your Gemfile and RSpec will automatically load them in a request spec.
A model spec is a thin wrapper for an ActiveSupport::TestCase, and includes all of the behavior and assertions that it provides, in addition to RSpec's own behavior and expectations.
Model spec
Model specs live in
A model spec is a thin wrapper for an ActiveSupport::TestCase, and includes all of the behavior and assertions that it provides, in addition to RSpec's own behavior and expectations.
Controller spec
Controller specs live in
A controller spec is an RSpec wrapper for a Rails functional test (ActionController::TestCase::Behavior). It allows you to simulate a single http request in each example, and then specify expected outcomes such as:
Routing spec
Routing specs live in the
Simple apps with nothing but standard RESTful routes won't get much value from routing specs, but they can provide significant value when used to specify customized routes, like vanity links, slugs, etc.
With request specs, you can:
- specify a single request
- specify multiple requests across multiple controllers
- specify multiple requests across multiple sessions
RSpec provides two matchers that delegate to Rails assertions:
render_template # delegates to assert_template redirect_to # delegates to assert_redirected_to
Check the Rails docs for details on these methods as well.If you would like to use webrat or capybara with your request specs, all you have to do is include one of them in your Gemfile and RSpec will automatically load them in a request spec.
-
- Given a file named "spec/requests/student_management_spec.rb" with:
require 'spec_helper' describe "Students" do describe "Student management" do it "creates a Student and redirects to the Student's page" do get "/students/new" response.should render_template(:new) post "/students", :student => {:name => "myname",:branch => 'cse',:marks => '548'} response.should redirect_to(assigns(:student)) follow_redirect! response.should render_template(:show) response.body.should include("Student was successfully created.") end it "display the values the Student's page" do get "/students" response.should render_template(:index) response.body.should include("Name") end end end
- When I run `rspec spec/requests/students_spec.rb`
- Then the example should pass
- Given a file named "spec/requests/student_management_spec.rb" with:
spec/models
or any example group with :type => :model
.A model spec is a thin wrapper for an ActiveSupport::TestCase, and includes all of the behavior and assertions that it provides, in addition to RSpec's own behavior and expectations.
Model spec
Model specs live in spec/models
or any example group with :type => :model
.A model spec is a thin wrapper for an ActiveSupport::TestCase, and includes all of the behavior and assertions that it provides, in addition to RSpec's own behavior and expectations.
Examples
require 'spec_helper' describe Student do context "with 3 subjects" do it "one to many between student and subject" do stud = Student.create(:name =>'subbarao',:branch =>'cse') stud.subjects<<Subject.create(:name =>"science",:teacher =>"Ram",:student_id =>stud.id) stud.subjects<<Subject.create(:name =>"Mths",:teacher =>"Sam",:student_id =>stud.id) stud.subjects<<Subject.create(:name =>"Computers",:teacher =>"Siva",:student_id =>stud.id) # stud.reload.subjects.should eq([subject3]) stud.should have(3).subjects end end class ValidatingStudent < ActiveRecord::Base set_table_name :students validates_presence_of :name validates_presence_of :branch end describe ValidatingStudent do it "fails validation with no name (using error_on)" do ValidatingStudent.new.should have(1).error_on(:name) end it "fails validation with no name (using errors_on)" do ValidatingStudent.new.should have(1).errors_on(:name) end it "passes validation with a name (using 0)" do ValidatingStudent.new(:name => "liquid nitrogen").should have(0).errors_on(:name) end it "passes validation with a name (using :no)" do ValidatingStudent.new(:name => "liquid nitrogen").should have(:no).errors_on(:name) end it "fails validation with no branch (using error_on)" do ValidatingStudent.new.should have(1).error_on(:branch) end it "fails validation with no branch (using errors_on)" do ValidatingStudent.new.should have(1).errors_on(:branch) end it "passes validation with a branch (using 0)" do ValidatingStudent.new(:branch => "liquid nitrogen").should have(0).errors_on(:branch) end it "passes validation with a branch (using :no)" do ValidatingStudent.new(:branch => "liquid nitrogen").should have(:no).errors_on(:branch) end end end
Controller spec
Controller specs live in spec/controllers
or any example group with :type => :controller
.A controller spec is an RSpec wrapper for a Rails functional test (ActionController::TestCase::Behavior). It allows you to simulate a single http request in each example, and then specify expected outcomes such as:
- rendered templates
- redirects
- instance variables assigned in the controller to be shared with the view
- cookies sent back with the response
- standard rspec matchers (
response.code.should eq(200)
) - standard test/unit assertions (
assert_equal 200, response.code
) - rails assertions (
assert_response 200
) - rails-specific matchers:
response.should render_template (wraps assert_template)
response.should redirect_to (wraps assert_redirected_to)
assigns(:widget).should be_a_new(Widget)
Examples
describe TeamsController do describe "GET index" do it "assigns @teams" do team = Team.create get :index assigns(:teams).should eq([team]) end it "renders the index template" do get :index response.should render_template("index") end end end
Views
- by default, views are not rendered. See views are stubbed by default and render_views for details.
helper spec
Helper specs live in
Helper specs expose a
To access the helper methods you're specifying, simply call them directly on the
NOTE: helper methods defined in controllers are not included.
spec/helpers
, or any example group with :type => :helper
.Helper specs expose a
helper
object, which includes the helper module being specified, the ApplicationHelper
module (if there is one) and all of the helpers built into Rails. It does not include the other helper modules in your app.To access the helper methods you're specifying, simply call them directly on the
helper
object.NOTE: helper methods defined in controllers are not included.
-
Scenario: helper method that returns a value
- Given a file named "spec/helpers/application_helper_spec.rb" with:
require "spec_helper" describe ApplicationHelper do describe "#page_title" do it "returns the default title" do helper.page_title.should eq("RSpec is your friend") end end end
- And a file named "app/helpers/application_helper.rb" with:
module ApplicationHelper def page_title "RSpec is your friend" end end
- When I run `rspec spec/helpers/application_helper_spec.rb`
- Then the examples should all pass
- Given a file named "spec/helpers/application_helper_spec.rb" with:
-
- Given a file named "spec/helpers/application_helper_spec.rb" with:
require "spec_helper" describe ApplicationHelper do describe "#page_title" do it "returns the instance variable" do assign(:title, "My Title") helper.page_title.should eql("My Title") end end end
- And a file named "app/helpers/application_helper.rb" with:
module ApplicationHelper def page_title @title || nil end end
- When I run `rspec spec/helpers/application_helper_spec.rb`
- Then the examples should all pass
- Given a file named "spec/helpers/application_helper_spec.rb" with:
-
- Given a file named "spec/helpers/widgets_helper_spec.rb" with:
require "spec_helper" describe WidgetsHelper do describe "#page_title" do it "includes the app name" do assign(:title, "This Page") helper.page_title.should eq("The App: This Page") end end end
- And a file named "app/helpers/application_helper.rb" with:
module ApplicationHelper def app_name "The App" end end
- And a file named "app/helpers/widgets_helper.rb" with:
module WidgetsHelper def page_title "#{app_name}: #{@title}" end end
- When I run `rspec spec/helpers/widgets_helper_spec.rb`
- Then the examples should all pass
- Given a file named "spec/helpers/widgets_helper_spec.rb" with:
URL helpers in mailer examples
-
Scenario: using URL helpers with default options
- Given a file named "config/initializers/mailer_defaults.rb" with:
Rails.configuration.action_mailer.default_url_options = { :host => 'example.com' }
- And a file named "spec/mailers/notifications_spec.rb" with:
require 'spec_helper' describe Notifications do it 'should have access to URL helpers' do lambda { gadgets_url }.should_not raise_error end end
- When I run `rspec spec`
- Then the examples should all pass
- Given a file named "config/initializers/mailer_defaults.rb" with:
-
- Given a file named "config/initializers/mailer_defaults.rb" with:
# no default options
- And a file named "spec/mailers/notifications_spec.rb" with:
require 'spec_helper' describe Notifications do it 'should have access to URL helpers' do lambda { gadgets_url :host => 'example.com' }.should_not raise_error lambda { gadgets_url }.should raise_error end
-
end
-
- When I run `rspec spec`
- Then the examples should all pass
- Given a file named "config/initializers/mailer_defaults.rb" with:
Routing spec
Routing specs live in the spec/routing
directory, or any example group with :type => :routing
.Simple apps with nothing but standard RESTful routes won't get much value from routing specs, but they can provide significant value when used to specify customized routes, like vanity links, slugs, etc.
{ :get => "/articles/2012/11/when-to-use-routing-specs" }. should route_to( :controller => "articles", :month => "2012-11", :slug => "when-to-use-routing-specs" )
They are also valuable for routes that should not be available:{ :delete => "/accounts/37" }.should_not be_routable
view spec
View specs live in spec/views and render view templates in isolation.
-
- Given a file named "spec/views/widgets/index.html.erb_spec.rb" with:
require "spec_helper" describe "widgets/index.html.erb" do it "displays all the widgets" do assign(:widgets, [ stub_model(Widget, :name => "slicer"), stub_model(Widget, :name => "dicer") ]) render rendered.should =~ /slicer/ rendered.should =~ /dicer/ end end
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/index.html.erb_spec.rb" with:
-
Scenario: passing spec with before and nesting
- Given a file named "spec/views/widgets/index.html.erb_spec.rb" with:
require "spec_helper" describe "widgets/index.html.erb" do context "with 2 widgets" do before(:each) do assign(:widgets, [ stub_model(Widget, :name => "slicer"), stub_model(Widget, :name => "dicer") ]) end it "displays both widgets" do render rendered.should =~ /slicer/ rendered.should =~ /dicer/ end end end
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/index.html.erb_spec.rb" with:
-
- Given a file named "spec/views/widgets/widget.html.erb_spec.rb" with:
require "spec_helper" describe "rendering the widget template" do it "displays the widget" do assign(:widget, stub_model(Widget, :name => "slicer")) render :template => "widgets/widget.html.erb" rendered.should =~ /slicer/ end end
- And a file named "app/views/widgets/widget.html.erb" with:
<h2><%= @widget.name %></h2>
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/widget.html.erb_spec.rb" with:
-
- Given a file named "spec/views/widgets/_widget.html.erb_spec.rb" with:
require "spec_helper" describe "rendering locals in a partial" do it "displays the widget" do widget = stub_model(Widget, :name => "slicer") render :partial => "widgets/widget.html.erb", :locals => {:widget => widget} rendered.should =~ /slicer/ end end
- And a file named "app/views/widgets/_widget.html.erb" with:
<h3><%= widget.name %></h3>
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/_widget.html.erb_spec.rb" with:
-
- Given a file named "spec/views/widgets/_widget.html.erb_spec.rb" with:
require "spec_helper" describe "rendering locals in a partial" do it "displays the widget" do widget = stub_model(Widget, :name => "slicer") render "widgets/widget", :widget => widget rendered.should =~ /slicer/ end end
- And a file named "app/views/widgets/_widget.html.erb" with:
<h3><%= widget.name %></h3>
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/_widget.html.erb_spec.rb" with:
-
Scenario: passing spec with rendering of text
- Given a file named "spec/views/widgets/direct.html.erb_spec.rb" with:
require "spec_helper" describe "rendering text directly" do it "displays the given text" do render :text => "This is directly rendered" rendered.should =~ /directly rendered/ end end
- When I run `rspec spec/views`
- Then the examples should all pass
- Given a file named "spec/views/widgets/direct.html.erb_spec.rb" with:
-
- Given a file named "app/views/secrets/index.html.erb" with:
<%- if admin? %> <h1>Secret admin area</h1> <%- end %>
- And a file named "spec/views/secrets/index.html.erb_spec.rb" with:
require 'spec_helper' describe 'secrets/index.html.erb' do before do controller.singleton_class.class_eval do protected def admin? true end helper_method :admin? end end it 'checks for admin access' do render rendered.should =~ /Secret admin area/ end end
- When I run `rspec spec/views/secrets`
- Then the examples should all pass
- Given a file named "app/views/secrets/index.html.erb" with:
MATCHERS
rspec-rails offers a number of custom matchers, most of which are rspec-compatible wrappers for Rails' assertions.redirects
# delegates to assert_redirected_to response.should redirect_to(path)
templates
# delegates to assert_template response.should render_template(template_name)
assigned objects
# passes if assigns(:widget) is an instance of Widget # and it is not persisted assigns(:widget).should be_a_new(Widget)
Comments
Post a Comment