Download - Ruby on Rails testing with Rspec
RSpec & RailsRSpec & RailsBunlong Van – Rubyist/Rails DeveloperBunlong Van – Rubyist/Rails DeveloperMail: Mail: [email protected]: http://geekhmer.github.ioBlog: 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) endend
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) endend
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 endend
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 endend
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 endend
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 < 6target.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_errorlambda {a_call}.should raise_error(<exception> [, message])lambda {a_call}.should_not raise_errorlambda {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>).thingstarget.should have_at_least(<number>).thingstarget.should have_at_most(<number>).thingstarget.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 endend
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 endend
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) endend
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") endend
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")
endend
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") endEnd
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")
endend
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") endend
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) # => nilUser.stub(:new).and_return(true)
user_object = User.newuser_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.paginateUser.stub_chain(:active, :paginate).and_return([user_object])
Stubs in action
User.stub(:new) # => nilUser.stub(:new).and_return(true)
user_object = User.newuser_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.paginateUser.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) # => nilUser.should_receive(:new).and_return(true)User.should_not_receive(:new)
user_object = User.newuser_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 # defaultuser_object.should_receive(:update_attributes).twiceuser_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" endend
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" endend
Question?