RSpec & Rails
Bunlong Van – Rubyist/Rails Developer
Mail: bunlong.van@gmail.com
Blog: http://geekhmer.github.io
Cover
- What is Rspec?
- RSpec features
- RSpec in action
- Stubs & Mocks
- Stubs & Mocks using RSpec
What is RSpec ?
- Testing framework for Ruby on Rails.
- Replacement for RoR built-in testing tool.
TestUnit
class Calculator < Test::Unit::TestCase
def test_addition
assert_equal(8, Calculator.new(6, 2).addition)
end
def test_subtraction
assert_same(4, Calculator.new(6, 2).subtraction)
end
end
RSpec
desribe Calculator do
let(:calculator) { Calculator.new(6,2) }
it "should return 8 when adding 6 and 2" do
calculator.addition.should eql(8)
end
it "should return 4 when subtracting 2 from 6" do
calculator.subtraction.should eql(4)
end
end
RSpec basics
describe MyClass do # creates initial scope
before do
@object = MyClass.new
end
describe "#some_method" do # creates new scope
it "should ..." do
@object.should ...
end
context "when authorized" do # creates new scope
before do
@object.authorized = true
end
RSpec basics (Con.)
it "should ..." do
@object.should ...
end
end
context "when not authorized" do # creates new scope
it "should ..." do
@object.should ...
end
end
end
it "should ..." do
pending "Fix some model first" # creates pending test
end
end
Why RSpec?
- Readability
- Tests documentation and failure messages
Readability
describe Campaign do
it "should be visible by default" do
campaign.should be_visible
end
it "should have performances visible by default" do
campaign.performances_visible.should be_true
end
it "should not be published at start" do
campaign.should_not be_published
end
Readability (Con.)
it "should not be ready to publish at start - without performances, etc" do
campaign.should_not be_ready_to_publish
end
describe "#allowed_performance_kinds" do
it "should allow all by default" do
campaign.allowed_performance_kinds.should == Performance.kinds
end
end
end
Tests documentation and failure
messages
When tests output matter
- Built-in profiler
- Test run filters
- Conventions
- Built-in matchers
Built-in profiler
Test run filters
def old_ruby
RUBY_VERSION != "1.9.2"
end
describe TrueClass do
it "should be true for true", :if => old_ruby do
true.should be_true
end
it "should be true for String", :current => true do
"".should be_true
end
Test run filters (Cons.)
it "should be true for Fixnum" do
0.should be_true
end
end
Conventions
Built-in matchers
target.should satisfy {|arg| ...}
target.should_not satisfy {|arg| ...}
target.should equal <value>
target.should not_equal <value>
target.should be_close <value>, <tolerance>
target.should_not be_close <value>, <tolerance>
target.should be <value>
target.should_not be <value>
target.should predicate [optional args]
target.should be_predicate [optional args]
target.should_not predicate [optional args]
target.should_not be_predicate [optional args]
target.should be < 6
target.should be_between(1, 10)
Built-in matchers (Cons.)
target.should match <regex>
target.should_not match <regex>
target.should be_an_instance_of <class>
target.should_not be_an_instance_of <class>
target.should be_a_kind_of <class>
target.should_not be_a_kind_of <class>
target.should respond_to <symbol>
target.should_not respond_to <symbol>
lambda {a_call}.should raise_error
lambda {a_call}.should raise_error(<exception> [, message])
lambda {a_call}.should_not raise_error
lambda {a_call}.should_not raise_error(<exception> [, message])
Built-in matchers (Cons.)
proc.should throw <symbol>
proc.should_not throw <symbol>
target.should include <object>
target.should_not include <object>
target.should have(<number>).things
target.should have_at_least(<number>).things
target.should have_at_most(<number>).things
target.should have(<number>).errors_on(:field)
expect { thing.approve! }.to change(thing, :status)
.from(Status::AWAITING_APPROVAL)
.to(Status::APPROVED)
expect { thing.destroy }.to change(Thing, :count).by(-1)
Problems ?
- Hard to learn at the beginning
- Routing tests could have more detailed failure messages
- Rails upgrade can break tests compatibility
RSpec in action
- Model specs (placed under spec/models director)
- Controller specs (placed under spec/controllers directory)
- Helper specs (placed under spec/helpers directory)
- View specs (placed under spec/views directory)
- Routing specs (placed under spec/routing directory)
Model specs
describe Campaign do
it "should be visible by default" do
campaign.should be_visible
end
it "should have performances visible by default" do
campaign.performances_visible.should be_true
end
it "should not be published at start" do
campaign.should_not be_published
end
Model specs (Cons.)
it "should not be ready to publish at start - without performances, etc" do
campaign.should_not be_ready_to_publish
end
describe "#allowed_performance_kinds" do
it "should allow all by default" do
campaign.allowed_performance_kinds.should == Performance.kinds
end
end
end
Controller specs
describe SessionsController do
render_views
describe "CREATE" do
context "for virtual user" do
before do
stub_find_user(virtual_user)
end
it "should not log into peoplejar" do
post :create, :user => {:email => virtual_user.email,
:password => virtual_user.password }
response.should_not redirect_to(myjar_dashboard_path)
end
end
Controller specs (Cons.)
context "for regular user" do
before do
stub_find_user(active_user)
end
it "should redirect to myjar when login data is correct" do
post :create, :user => {:email => active_user.email,
:password => active_user.password }
response.should redirect_to(myjar_dashboard_path)
end
end
end
end
Helper specs
describe CampaignsHelper do
let(:campaign) { Factory.stub(:campaign) }
let(:file_name) { "meldung.jpg" }
it "should return the same attachment URL as paperclip
if there is no attachment" do
campaign.stub(:featured_image_file_name).and_return(nil)
helper.campaign_attachment_url(campaign, :featured_image).
should eql(campaign.featured_image.url)
end
it "should return the same attachment URL as paperclip if there
is attachment" do
campaign.stub(:featured_image_file_name).and_return(file_name)
Helper specs (Cons.)
helper.campaign_attachment_url(campaign, :featured_image).
should eql(campaign.featured_image.url)
end
end
View specs
# view at views/campaigns/index.html.erb
<%= content_for :actions do %>
<div id="hb_actions" class="browse_arena">
<div id="middle_actions">
<ul class="btn">
<li class="btn_blue"><%= create_performance_link %></li>
</ul>
</div>
</div>
<% end %>
<div id="interest_board_holder">
<%= campaings_wall_template(@campaigns) %>
</div>
View specs (Cons.)
# spec at spec/views/campaigns/index.html.erb_spec.rb
describe "campaigns/index.html.erb" do
let(:campaign) { Factory.stub(:campaign) }
it "displays pagination when there are more than 20 published
campaigns" do
assign(:campaigns, (1..21).map { campaign }.
paginate(:per_page => 2) )
render
rendered.should include("Prev")
rendered.should include("Next")
end
end
Routing specs
describe "home routing", :type => :controller do
it "should route / to Home#index" do
{ :get => "/" }.should route_to(:controller => "home", :action => "index",
:subdomain => false)
end
it "should route / with subdomain to Performances::Performances#index
do
{ :get => "http://kzkgop.test.peoplejar.net" }.
should route_to(:namespace => nil, :controller =>
"performances/performances", :action => "index")
end
end
Routing specs (Cons.)
describe "error routing", :type => :controller do
it "should route not existing route Errors#new" do
{ :get => "/not_existing_route" }.should route_to(:controller => "errors",
:action => "new", :path => "not_existing_route")
end
End
describe "icebreaks routing" do
it "should route /myjar/icebreaks/initiated to
Icebreaks::InitiatedIcebreaks#index" do
{ :get => "/myjar/icebreaks/initiated" }.should
route_to(:controller => "icebreaks/initiated_icebreaks",
:action => "index")
end
end
Routing specs (Cons.)
describe "admin routing" do
it "should route /admin to Admin::Base#index" do
{ :get => "/admin" }.should route_to(:controller => "admin/welcome",
:action => "index")
end
end
Stubs & Mocks
Back to unit test assumptions
- A unit is the smallest testable part of an application
- The goal of unit testing is to isolate each part of the program and show
that the individual parts are correct
- Ideally, each test case is independent from the others
you.should use_stubs!
- Isolate your unit tests from external libraries and dependencies
- Propagate skinny methods which has low responsibility
- Single bug should make only related tests fail
- Speed up tests
PeopleJar is using
Are there any problems ?
- Writing test is more time consuming
- Need to know stubbed library internal implementations
- Need to write an integration test first
Stubs in action
User.stub(:new) # => nil
User.stub(:new).and_return(true)
user_object = User.new
user_object.stub(:save).and_return(true)
User.stub(:new).and_return(user_object)
user_object.stub(:update_attributes).with(:username => "test").
and_return(true)
User.stub(:new).and_return(user_object)
User.any_instance.stub(:save).and_return(true)
# User.active.paginate
User.stub_chain(:active, :paginate).and_return([user_object])
Stubs in action
User.stub(:new) # => nil
User.stub(:new).and_return(true)
user_object = User.new
user_object.stub(:save).and_return(true)
User.stub(:new).and_return(user_object)
user_object.stub(:update_attributes).with(:username => "test").
and_return(true)
User.stub(:new).and_return(user_object)
User.any_instance.stub(:save).and_return(true)
# User.active.paginate
User.stub_chain(:active, :paginate).and_return([user_object])
Stubs in action (Cons.)
user_object.stub(:set_permissions).with(an_instance_of(String),
anything).and_return(true)
user_object.unstub(:set_permissions)
# user_object.set_permissions("admin", true) # => true (will use stubbed
method)
# user_object.set_permissions("admin") # => false (will call real method)
Mocks in action
User.should_receive(:new) # => nil
User.should_receive(:new).and_return(true)
User.should_not_receive(:new)
user_object = User.new
user_object.should_receive(:save).and_return(true)
User.stub(:new).and_return(user_object)
user_object.should_receive(:update_attributes).
with(:username => "test").and_return(true)
User.stub(:new).and_return(user_object)
User.any_instance.should_receive(:save).and_return(true) # !
Mocks in action (Cons.)
user_object.should_receive(:update_attributes).once # default
user_object.should_receive(:update_attributes).twice
user_object.should_receive(:update_attributes).exactly(3).times
user_object.should_receive(:set_permissions).
with(an_instance_of(String), anything)
# user_object.set_permissions("admin", true) # Success
# user_object.set_permissions("admin") # Fail
What's the difference between
Stubs and Mocks
- Mocks are used to define expectations and verify them
- Stubs allows for defining eligible behavior
- Stubs will not cause a test to fail due to unfulfilled expectation
In practice - Stub failure
describe ".to_csv_file" do
it "should generate CSV output" do
User.stub(:active).and_return([user])
User.to_csv_file.should == "#{user.display_name},#{user.email}n"
end
end
In practice - Mock failure
describe "#facebook_uid=" do
it "should build facebook setting instance if not exists when setting uid"
do
user.should_receive(:build_facebook_setting).with(:uid => "123")
user.facebook_uid = "123"
end
end
Question?

Ruby on Rails testing with Rspec

  • 1.
    RSpec & Rails BunlongVan – Rubyist/Rails Developer Mail: bunlong.van@gmail.com Blog: http://geekhmer.github.io
  • 2.
    Cover - What isRspec? - RSpec features - RSpec in action - Stubs & Mocks - Stubs & Mocks using RSpec
  • 3.
    What is RSpec? - Testing framework for Ruby on Rails. - Replacement for RoR built-in testing tool.
  • 4.
    TestUnit class Calculator <Test::Unit::TestCase def test_addition assert_equal(8, Calculator.new(6, 2).addition) end def test_subtraction assert_same(4, Calculator.new(6, 2).subtraction) end end
  • 5.
    RSpec desribe Calculator do let(:calculator){ Calculator.new(6,2) } it "should return 8 when adding 6 and 2" do calculator.addition.should eql(8) end it "should return 4 when subtracting 2 from 6" do calculator.subtraction.should eql(4) end end
  • 6.
    RSpec basics describe MyClassdo # creates initial scope before do @object = MyClass.new end describe "#some_method" do # creates new scope it "should ..." do @object.should ... end context "when authorized" do # creates new scope before do @object.authorized = true end
  • 7.
    RSpec basics (Con.) it"should ..." do @object.should ... end end context "when not authorized" do # creates new scope it "should ..." do @object.should ... end end end it "should ..." do pending "Fix some model first" # creates pending test end end
  • 8.
    Why RSpec? - Readability -Tests documentation and failure messages
  • 9.
    Readability describe Campaign do it"should be visible by default" do campaign.should be_visible end it "should have performances visible by default" do campaign.performances_visible.should be_true end it "should not be published at start" do campaign.should_not be_published end
  • 10.
    Readability (Con.) it "shouldnot be ready to publish at start - without performances, etc" do campaign.should_not be_ready_to_publish end describe "#allowed_performance_kinds" do it "should allow all by default" do campaign.allowed_performance_kinds.should == Performance.kinds end end end
  • 11.
    Tests documentation andfailure messages
  • 12.
    When tests outputmatter - Built-in profiler - Test run filters - Conventions - Built-in matchers
  • 13.
  • 14.
    Test run filters defold_ruby RUBY_VERSION != "1.9.2" end describe TrueClass do it "should be true for true", :if => old_ruby do true.should be_true end it "should be true for String", :current => true do "".should be_true end
  • 15.
    Test run filters(Cons.) it "should be true for Fixnum" do 0.should be_true end end
  • 16.
  • 17.
    Built-in matchers target.should satisfy{|arg| ...} target.should_not satisfy {|arg| ...} target.should equal <value> target.should not_equal <value> target.should be_close <value>, <tolerance> target.should_not be_close <value>, <tolerance> target.should be <value> target.should_not be <value> target.should predicate [optional args] target.should be_predicate [optional args] target.should_not predicate [optional args] target.should_not be_predicate [optional args] target.should be < 6 target.should be_between(1, 10)
  • 18.
    Built-in matchers (Cons.) target.shouldmatch <regex> target.should_not match <regex> target.should be_an_instance_of <class> target.should_not be_an_instance_of <class> target.should be_a_kind_of <class> target.should_not be_a_kind_of <class> target.should respond_to <symbol> target.should_not respond_to <symbol> lambda {a_call}.should raise_error lambda {a_call}.should raise_error(<exception> [, message]) lambda {a_call}.should_not raise_error lambda {a_call}.should_not raise_error(<exception> [, message])
  • 19.
    Built-in matchers (Cons.) proc.shouldthrow <symbol> proc.should_not throw <symbol> target.should include <object> target.should_not include <object> target.should have(<number>).things target.should have_at_least(<number>).things target.should have_at_most(<number>).things target.should have(<number>).errors_on(:field) expect { thing.approve! }.to change(thing, :status) .from(Status::AWAITING_APPROVAL) .to(Status::APPROVED) expect { thing.destroy }.to change(Thing, :count).by(-1)
  • 20.
    Problems ? - Hardto learn at the beginning - Routing tests could have more detailed failure messages - Rails upgrade can break tests compatibility
  • 21.
    RSpec in action -Model specs (placed under spec/models director) - Controller specs (placed under spec/controllers directory) - Helper specs (placed under spec/helpers directory) - View specs (placed under spec/views directory) - Routing specs (placed under spec/routing directory)
  • 22.
    Model specs describe Campaigndo it "should be visible by default" do campaign.should be_visible end it "should have performances visible by default" do campaign.performances_visible.should be_true end it "should not be published at start" do campaign.should_not be_published end
  • 23.
    Model specs (Cons.) it"should not be ready to publish at start - without performances, etc" do campaign.should_not be_ready_to_publish end describe "#allowed_performance_kinds" do it "should allow all by default" do campaign.allowed_performance_kinds.should == Performance.kinds end end end
  • 24.
    Controller specs describe SessionsControllerdo render_views describe "CREATE" do context "for virtual user" do before do stub_find_user(virtual_user) end it "should not log into peoplejar" do post :create, :user => {:email => virtual_user.email, :password => virtual_user.password } response.should_not redirect_to(myjar_dashboard_path) end end
  • 25.
    Controller specs (Cons.) context"for regular user" do before do stub_find_user(active_user) end it "should redirect to myjar when login data is correct" do post :create, :user => {:email => active_user.email, :password => active_user.password } response.should redirect_to(myjar_dashboard_path) end end end end
  • 26.
    Helper specs describe CampaignsHelperdo let(:campaign) { Factory.stub(:campaign) } let(:file_name) { "meldung.jpg" } it "should return the same attachment URL as paperclip if there is no attachment" do campaign.stub(:featured_image_file_name).and_return(nil) helper.campaign_attachment_url(campaign, :featured_image). should eql(campaign.featured_image.url) end it "should return the same attachment URL as paperclip if there is attachment" do campaign.stub(:featured_image_file_name).and_return(file_name)
  • 27.
    Helper specs (Cons.) helper.campaign_attachment_url(campaign,:featured_image). should eql(campaign.featured_image.url) end end
  • 28.
    View specs # viewat views/campaigns/index.html.erb <%= content_for :actions do %> <div id="hb_actions" class="browse_arena"> <div id="middle_actions"> <ul class="btn"> <li class="btn_blue"><%= create_performance_link %></li> </ul> </div> </div> <% end %> <div id="interest_board_holder"> <%= campaings_wall_template(@campaigns) %> </div>
  • 29.
    View specs (Cons.) #spec at spec/views/campaigns/index.html.erb_spec.rb describe "campaigns/index.html.erb" do let(:campaign) { Factory.stub(:campaign) } it "displays pagination when there are more than 20 published campaigns" do assign(:campaigns, (1..21).map { campaign }. paginate(:per_page => 2) ) render rendered.should include("Prev") rendered.should include("Next") end end
  • 30.
    Routing specs describe "homerouting", :type => :controller do it "should route / to Home#index" do { :get => "/" }.should route_to(:controller => "home", :action => "index", :subdomain => false) end it "should route / with subdomain to Performances::Performances#index do { :get => "http://kzkgop.test.peoplejar.net" }. should route_to(:namespace => nil, :controller => "performances/performances", :action => "index") end end
  • 31.
    Routing specs (Cons.) describe"error routing", :type => :controller do it "should route not existing route Errors#new" do { :get => "/not_existing_route" }.should route_to(:controller => "errors", :action => "new", :path => "not_existing_route") end End describe "icebreaks routing" do it "should route /myjar/icebreaks/initiated to Icebreaks::InitiatedIcebreaks#index" do { :get => "/myjar/icebreaks/initiated" }.should route_to(:controller => "icebreaks/initiated_icebreaks", :action => "index") end end
  • 32.
    Routing specs (Cons.) describe"admin routing" do it "should route /admin to Admin::Base#index" do { :get => "/admin" }.should route_to(:controller => "admin/welcome", :action => "index") end end
  • 33.
  • 34.
    Back to unittest assumptions - A unit is the smallest testable part of an application - The goal of unit testing is to isolate each part of the program and show that the individual parts are correct - Ideally, each test case is independent from the others
  • 35.
    you.should use_stubs! - Isolateyour unit tests from external libraries and dependencies - Propagate skinny methods which has low responsibility - Single bug should make only related tests fail - Speed up tests
  • 36.
  • 37.
    Are there anyproblems ? - Writing test is more time consuming - Need to know stubbed library internal implementations - Need to write an integration test first
  • 38.
    Stubs in action User.stub(:new)# => nil User.stub(:new).and_return(true) user_object = User.new user_object.stub(:save).and_return(true) User.stub(:new).and_return(user_object) user_object.stub(:update_attributes).with(:username => "test"). and_return(true) User.stub(:new).and_return(user_object) User.any_instance.stub(:save).and_return(true) # User.active.paginate User.stub_chain(:active, :paginate).and_return([user_object])
  • 39.
    Stubs in action User.stub(:new)# => nil User.stub(:new).and_return(true) user_object = User.new user_object.stub(:save).and_return(true) User.stub(:new).and_return(user_object) user_object.stub(:update_attributes).with(:username => "test"). and_return(true) User.stub(:new).and_return(user_object) User.any_instance.stub(:save).and_return(true) # User.active.paginate User.stub_chain(:active, :paginate).and_return([user_object])
  • 40.
    Stubs in action(Cons.) user_object.stub(:set_permissions).with(an_instance_of(String), anything).and_return(true) user_object.unstub(:set_permissions) # user_object.set_permissions("admin", true) # => true (will use stubbed method) # user_object.set_permissions("admin") # => false (will call real method)
  • 41.
    Mocks in action User.should_receive(:new)# => nil User.should_receive(:new).and_return(true) User.should_not_receive(:new) user_object = User.new user_object.should_receive(:save).and_return(true) User.stub(:new).and_return(user_object) user_object.should_receive(:update_attributes). with(:username => "test").and_return(true) User.stub(:new).and_return(user_object) User.any_instance.should_receive(:save).and_return(true) # !
  • 42.
    Mocks in action(Cons.) user_object.should_receive(:update_attributes).once # default user_object.should_receive(:update_attributes).twice user_object.should_receive(:update_attributes).exactly(3).times user_object.should_receive(:set_permissions). with(an_instance_of(String), anything) # user_object.set_permissions("admin", true) # Success # user_object.set_permissions("admin") # Fail
  • 43.
    What's the differencebetween Stubs and Mocks - Mocks are used to define expectations and verify them - Stubs allows for defining eligible behavior - Stubs will not cause a test to fail due to unfulfilled expectation
  • 44.
    In practice -Stub failure describe ".to_csv_file" do it "should generate CSV output" do User.stub(:active).and_return([user]) User.to_csv_file.should == "#{user.display_name},#{user.email}n" end end
  • 45.
    In practice -Mock failure describe "#facebook_uid=" do it "should build facebook setting instance if not exists when setting uid" do user.should_receive(:build_facebook_setting).with(:uid => "123") user.facebook_uid = "123" end end
  • 46.