SlideShare a Scribd company logo
How To Test
 Everything

             Noel Rappin
   Pathfinder Development
How To Test
 Everything
... In Rails
              Noel Rappin
    Pathfinder Development
Guidelines
The First Guideline


Any change to the logic of the
program should be driven by a failed
test
The Second Guideline



A test should be as close as possible
to the associated code.
The Third Guideline



Test features and functionality, not
code
The Fourth Guideline


Rails works. (Mostly).

You don’t need to test it.
Framework
What Framework To
       Use?
What Framework To
         Use?
Short Answer: I don’t care
What Framework To
         Use?
Short Answer: I don’t care

Longer Answer: Start with Rails Core,
move when you have an unfulfilled
need
What Framework To
         Use?
Short Answer: I don’t care

Longer Answer: Start with Rails Core,
move when you have an unfulfilled
need

Those needs: Contexts, Factories,
Mocks
Models
Associations
Associations
No need to just check for the existence
of an association

Associations should be driven by
failing tests of actual functionality

Code extensions in an association
block should be treated like any other
code
Named
Scopes
Named Scopes

Named scopes are methods

Don’t test that a named scope has the
SQL decorations you put in

Do test that the scope correctly finds
or manages the objects you expect
Named Scope Tests
Named Scope Tests

assert_same_elements [@melvin, @thomas],
 User.primary_status
Named Scope Tests

 assert_same_elements [@melvin, @thomas],
  User.primary_status



should_have_named_scope :primary_status do |u|
 ["online", "away"]include?(u.status)
end
Should Have Scope
def self.should_match_named_scope(named_scope,
   *args, &block)
 should "match named scope #{named_scope}" do
  ar_class = self.class.model_class
  scoped_objects = ar_class.send(named_scope, *args)
  assert !scoped_objects.blank?
  scoped_objects.each do |obj|
    assert block.call(obj)
  end

  non_scoped_objects = ar_class.all - scoped_objects
  assert !non_scoped_objects.blank?
  non_scoped_objects.each do |obj|
    assert !block.call(obj)
  end
 end
end
Validations
Validations

Don’t test Rails code

Do test for valid state

Do test anything custom

And anything with a regex

Failing saves can lead to irritating test
Controllers
Filters
Filters

Generally, test as part of actual action

Don’t need to re-test refactors

Failing filters are a big cause of silent
test failures
Views
Views

Test for semantic structure (DOM ID,
class)

assert_select is your friend

Sometimes, not test first
Test for the Negative
What’s not there is as important as
what is
Test for the Negative
What’s not there is as important as
what is

   assert_select "#edit_link", :count => 0
Test for the Negative
What’s not there is as important as
what is

   assert_select "#edit_link", :count => 0




  assert_select "li:not(#edit_link)", :count => 2
Stupid assert_select
       Tricks
Stupid assert_select
       Tricks
      assert_select "input[name *= phone]"
Stupid assert_select
         Tricks
                   assert_select "input[name *= phone]"


assert_select "li#?", dom_id(@user, :item), :count => 1
Stupid assert_select
            Tricks
                       assert_select "input[name *= phone]"


   assert_select "li#?", dom_id(@user, :item), :count => 1


assert_select "ul#directory_list" do
 assert_select "li:nth-of-type(1)",
    :text => "Albert Aardvark"
 assert_select "li:nth-of-type(2)",
    :text => "Zack Zebra"
end
Helpers
Helpers

DO TEST HELPERS

Auto generated in Rails 2.3 and up

test/unit/helpers

Methods like url_for can be stubbed in
the test class
       class UsersHelperTest < ActionView::TestCase
       end
Testing Block Helpers
Testing Block Helpers
def if_logged_in
 yield if logged_in?
end
Testing Block Helpers
def if_logged_in
 yield if logged_in?
end


                       <% if_logged_in do %>
                        <%= link_to "logout", logout_path %>
                       <% end %>
Testing Block Helpers
def if_logged_in
 yield if logged_in?
end


                       <% if_logged_in do %>
                        <%= link_to "logout", logout_path %>
                       <% end %>
    test "logged_in" do
     assert !logged_in?
     assert_nil(if_logged_in {"logged in"})
     login_as users(:quentin)
     assert logged_in?
     assert_equal("logged in", if_logged_in {"logged in"})
    end
Testing Output
   Helpers
Testing Output
def make_headline
                  Helpers
 concat("<h1 class='headline'>#{yield}</h1>")
end
Testing Output
def make_headline
                  Helpers
 concat("<h1 class='headline'>#{yield}</h1>")
end

test "make headline" do
 assert_dom_equal("<h1 class='headline'>fred</h1>",
    make_headline { "fred" })
end
Testing Output
def make_headline
                  Helpers
 concat("<h1 class='headline'>#{yield}</h1>")
end

test "make headline" do
 assert_dom_equal("<h1 class='headline'>fred</h1>",
    make_headline { "fred" })
end

test "make headline with output buffer" do
 make_headline { "fred" }
 assert_dom_equal("<h1 class='headline'>fred</h1>",
    output_buffer)
end
assert_select helpers
assert_select helpers
setup :setup_response
def setup_response
 @output_buffer = ""
 @request = ActionController::TestRequest.new
 @response = ActionController::TestResponse.new
end

def make_response(text)
 @response.body = text
end
assert_select helpers
setup :setup_response
def setup_response
 @output_buffer = ""
 @request = ActionController::TestRequest.new
 @response = ActionController::TestResponse.new
end

def make_response(text)
 @response.body = text
end
            test "make headline with response body" do
             make_headline { "fred" }
             make_response output_buffer
             assert_select("h1.headline")
            end
Email
Email
           ActionMailer::Base.deliveries.clear

Treat emails like views

assert_select_email

email_spec plugin for Cucumber &
RSpec

Shoulda: assert_did_not_sent_email,
assert_sent_email
Email
             ActionMailer::Base.deliveries.clear

Treat emails like views

assert_select_email

email_spec plugin for Cucumber &
RSpec

Shoulda: assert_did_not_sent_email, do
                 should "send an email to mom"
assert_sent_email assert_sent_email do |email|
                        email.to == "mom@mommy.com"
                       end
                      end
User
Interaction
Multi-User Interaction
Integration tests
 test "user interaction" do
  my_session = open_session
  your_session = open_session
  my_session.post("messages/send", :to => you)
  your_session.get("messages/show")
  assert_equal 1, your_session.assigns(:messages).size
 end
Ajax
Ajax



assert_select_rjs, but only for Rails
stuff
      test "an ajax call"
       xhr get :create
       assert_select_rjs :replace, :dom_id, 12
      end
Blue Ridge /
                 Screw.Unit
Screw.Unit(function() {
  describe("With my search box and default", function() {
    it("should switch the default", function() {
      search_focus($('#search'));
      expect($('#search').attr('value')).to(equal, '');
    });
  });
});
External Sites
Third Party Sites/ Web

Mock, Mock, Mock, Mock

Encapsulate call into an easily-mocked
method

Remember to test failure response
Rake
Rake Tasks
Encapsulate the task into a class/
method and test that method
Rake Tasks
Encapsulate the task into a class/
method and test that method
     test "my rake task" do
      @rake = Rake::Application.new
      Rake.application = @rake
      Rake.application.rake_require "lib/tasks/app"
      Rake::Task.define_task(:environment)
      @rake[:task_name].invoke
     end
                                    http://
                                www.philsergi.com
Dates and
  Time
Date/Time
Timecop
Date/Time
Timecop
          setup :timecop_freeze
          teardown :timecop_return

          def timecop_freeze
           Timecop.freeze(Time.now)
          end

          def timecop_return
           Timecop.return
          end
Date/Time
Timecop
            setup :timecop_freeze
            teardown :timecop_return

            def timecop_freeze
             Timecop.freeze(Time.now)
            end

            def timecop_return
             Timecop.return
            end


   Timecop.freeze(Date.parse('8 April 2009').to_time)
File Upload
File Uploads



fixture_file_upload
post :update,
   :image => fixture_file_upload(
       '/test/fixtures/face.png', 'image/png')
Rack
Rails Metal
Rails Metal
         Rack Middleware
Integration tests and cucumber

Rack::Test
def test_redirect_logged_in_users_to_dashboard
  authorize "bryan", "secret"
  get "/"
  follow_redirect!
  assert_equal "http://example.org/redirected",
   last_request.url
  assert last_response.ok?
 end
Legacy
Applications
Legacy Apps
Acceptance is the first step

Make sure the suite runs

Try black-box tests

Try mock objects

Look for seams

Don’t look backward
Wrap Up
Wrap Up

Code should flow from failed tests

Test features and functionality

Look for tools to help

The goal is great code that delivers
value
For More Info
http://speakerrate.com/events/183

http://www.pathf.com/blogs

http://blog.railsrx.com

http://www.twitter.com/noelrap

http://www.pragprog.com/titles/

More Related Content

How To Test Everything

  • 1. How To Test Everything Noel Rappin Pathfinder Development
  • 2. How To Test Everything ... In Rails Noel Rappin Pathfinder Development
  • 4. The First Guideline Any change to the logic of the program should be driven by a failed test
  • 5. The Second Guideline A test should be as close as possible to the associated code.
  • 6. The Third Guideline Test features and functionality, not code
  • 7. The Fourth Guideline Rails works. (Mostly). You don’t need to test it.
  • 10. What Framework To Use? Short Answer: I don’t care
  • 11. What Framework To Use? Short Answer: I don’t care Longer Answer: Start with Rails Core, move when you have an unfulfilled need
  • 12. What Framework To Use? Short Answer: I don’t care Longer Answer: Start with Rails Core, move when you have an unfulfilled need Those needs: Contexts, Factories, Mocks
  • 15. Associations No need to just check for the existence of an association Associations should be driven by failing tests of actual functionality Code extensions in an association block should be treated like any other code
  • 17. Named Scopes Named scopes are methods Don’t test that a named scope has the SQL decorations you put in Do test that the scope correctly finds or manages the objects you expect
  • 19. Named Scope Tests assert_same_elements [@melvin, @thomas], User.primary_status
  • 20. Named Scope Tests assert_same_elements [@melvin, @thomas], User.primary_status should_have_named_scope :primary_status do |u| ["online", "away"]include?(u.status) end
  • 21. Should Have Scope def self.should_match_named_scope(named_scope, *args, &block) should "match named scope #{named_scope}" do ar_class = self.class.model_class scoped_objects = ar_class.send(named_scope, *args) assert !scoped_objects.blank? scoped_objects.each do |obj| assert block.call(obj) end non_scoped_objects = ar_class.all - scoped_objects assert !non_scoped_objects.blank? non_scoped_objects.each do |obj| assert !block.call(obj) end end end
  • 23. Validations Don’t test Rails code Do test for valid state Do test anything custom And anything with a regex Failing saves can lead to irritating test
  • 26. Filters Generally, test as part of actual action Don’t need to re-test refactors Failing filters are a big cause of silent test failures
  • 27. Views
  • 28. Views Test for semantic structure (DOM ID, class) assert_select is your friend Sometimes, not test first
  • 29. Test for the Negative What’s not there is as important as what is
  • 30. Test for the Negative What’s not there is as important as what is assert_select "#edit_link", :count => 0
  • 31. Test for the Negative What’s not there is as important as what is assert_select "#edit_link", :count => 0 assert_select "li:not(#edit_link)", :count => 2
  • 33. Stupid assert_select Tricks assert_select "input[name *= phone]"
  • 34. Stupid assert_select Tricks assert_select "input[name *= phone]" assert_select "li#?", dom_id(@user, :item), :count => 1
  • 35. Stupid assert_select Tricks assert_select "input[name *= phone]" assert_select "li#?", dom_id(@user, :item), :count => 1 assert_select "ul#directory_list" do assert_select "li:nth-of-type(1)", :text => "Albert Aardvark" assert_select "li:nth-of-type(2)", :text => "Zack Zebra" end
  • 37. Helpers DO TEST HELPERS Auto generated in Rails 2.3 and up test/unit/helpers Methods like url_for can be stubbed in the test class class UsersHelperTest < ActionView::TestCase end
  • 39. Testing Block Helpers def if_logged_in yield if logged_in? end
  • 40. Testing Block Helpers def if_logged_in yield if logged_in? end <% if_logged_in do %> <%= link_to "logout", logout_path %> <% end %>
  • 41. Testing Block Helpers def if_logged_in yield if logged_in? end <% if_logged_in do %> <%= link_to "logout", logout_path %> <% end %> test "logged_in" do assert !logged_in? assert_nil(if_logged_in {"logged in"}) login_as users(:quentin) assert logged_in? assert_equal("logged in", if_logged_in {"logged in"}) end
  • 42. Testing Output Helpers
  • 43. Testing Output def make_headline Helpers concat("<h1 class='headline'>#{yield}</h1>") end
  • 44. Testing Output def make_headline Helpers concat("<h1 class='headline'>#{yield}</h1>") end test "make headline" do assert_dom_equal("<h1 class='headline'>fred</h1>", make_headline { "fred" }) end
  • 45. Testing Output def make_headline Helpers concat("<h1 class='headline'>#{yield}</h1>") end test "make headline" do assert_dom_equal("<h1 class='headline'>fred</h1>", make_headline { "fred" }) end test "make headline with output buffer" do make_headline { "fred" } assert_dom_equal("<h1 class='headline'>fred</h1>", output_buffer) end
  • 47. assert_select helpers setup :setup_response def setup_response @output_buffer = "" @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def make_response(text) @response.body = text end
  • 48. assert_select helpers setup :setup_response def setup_response @output_buffer = "" @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def make_response(text) @response.body = text end test "make headline with response body" do make_headline { "fred" } make_response output_buffer assert_select("h1.headline") end
  • 49. Email
  • 50. Email ActionMailer::Base.deliveries.clear Treat emails like views assert_select_email email_spec plugin for Cucumber & RSpec Shoulda: assert_did_not_sent_email, assert_sent_email
  • 51. Email ActionMailer::Base.deliveries.clear Treat emails like views assert_select_email email_spec plugin for Cucumber & RSpec Shoulda: assert_did_not_sent_email, do should "send an email to mom" assert_sent_email assert_sent_email do |email| email.to == "mom@mommy.com" end end
  • 53. Multi-User Interaction Integration tests test "user interaction" do my_session = open_session your_session = open_session my_session.post("messages/send", :to => you) your_session.get("messages/show") assert_equal 1, your_session.assigns(:messages).size end
  • 54. Ajax
  • 55. Ajax assert_select_rjs, but only for Rails stuff test "an ajax call" xhr get :create assert_select_rjs :replace, :dom_id, 12 end
  • 56. Blue Ridge / Screw.Unit Screw.Unit(function() { describe("With my search box and default", function() { it("should switch the default", function() { search_focus($('#search')); expect($('#search').attr('value')).to(equal, ''); }); }); });
  • 58. Third Party Sites/ Web Mock, Mock, Mock, Mock Encapsulate call into an easily-mocked method Remember to test failure response
  • 59. Rake
  • 60. Rake Tasks Encapsulate the task into a class/ method and test that method
  • 61. Rake Tasks Encapsulate the task into a class/ method and test that method test "my rake task" do @rake = Rake::Application.new Rake.application = @rake Rake.application.rake_require "lib/tasks/app" Rake::Task.define_task(:environment) @rake[:task_name].invoke end http:// www.philsergi.com
  • 62. Dates and Time
  • 64. Date/Time Timecop setup :timecop_freeze teardown :timecop_return def timecop_freeze Timecop.freeze(Time.now) end def timecop_return Timecop.return end
  • 65. Date/Time Timecop setup :timecop_freeze teardown :timecop_return def timecop_freeze Timecop.freeze(Time.now) end def timecop_return Timecop.return end Timecop.freeze(Date.parse('8 April 2009').to_time)
  • 67. File Uploads fixture_file_upload post :update, :image => fixture_file_upload( '/test/fixtures/face.png', 'image/png')
  • 69. Rails Metal Rack Middleware Integration tests and cucumber Rack::Test def test_redirect_logged_in_users_to_dashboard authorize "bryan", "secret" get "/" follow_redirect! assert_equal "http://example.org/redirected", last_request.url assert last_response.ok? end
  • 71. Legacy Apps Acceptance is the first step Make sure the suite runs Try black-box tests Try mock objects Look for seams Don’t look backward
  • 73. Wrap Up Code should flow from failed tests Test features and functionality Look for tools to help The goal is great code that delivers value