Don T Mock Yourself Out Presentation
Upcoming SlideShare
Loading in...5
×
 

Don T Mock Yourself Out Presentation

on

  • 2,872 views

 

Statistics

Views

Total Views
2,872
Views on SlideShare
2,868
Embed Views
4

Actions

Likes
7
Downloads
45
Comments
0

1 Embed 4

http://www.slideshare.net 4

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Don T Mock Yourself Out Presentation Don T Mock Yourself Out Presentation Presentation Transcript

  • Don’t Mock Yourself Out David Chelimsky Articulated Man, Inc
  • http://martinfowler.com/articles/mocksArentStubs.html
  • Classical and Mockist Testing
  • Classical and Mockist Testing
  • Classical and Mockist Testing
  • classicist mockist
  • merbist railsist
  • rspecist testunitist
  • ist bin ein red herring
  • The big issue here is when to use a mock http://martinfowler.com/articles/mocksArentStubs.html
  • agenda ๏ overview of stubs and mocks ๏ mocks/stubs applied to rails ๏ guidelines and pitfalls ๏ questions
  • test double
  • test stub describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end
  • test stub describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end
  • test stub describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end
  • test stub describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end
  • mock object describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • mock object describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • mock object describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • mock object describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • mock object describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • method level concepts
  • describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • method stub describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • message expectation describe Statement do it quot;logs a message when printedquot; do customer = Object.new logger = Object.new customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • things aren’t always as they seem
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end message expectation class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = mock(quot;customerquot;) statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer') statement.header.should == quot;Statement for Joe Customerquot; end end bound to implementation class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end ???? class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end message expectation class Statement def header quot;Statement for #{@customer.name}quot; end end
  • describe Statement do it quot;uses the customer name in the headerquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer) statement.header.should == quot;Statement for Joe Customerquot; end end bound to implementation class Statement def header quot;Statement for #{@customer.name}quot; end end
  • stubs are often used like mocks
  • mocks are often used like stubs
  • we verify stubs by checking state after an action
  • we tell mocks to verify interactions
  • sometimes stubs just make the system run
  • when are method stubs helpful?
  • isolation from non-determinism
  • random values
  • random values class BoardTest < MiniTest::Unit::TestCase def test_allows_move_to_last_square board = Board.new( :squares => 50, :die => MiniTest::Mock.new.expect('roll', 2) ) piece = Piece.new board.place(piece, 48) board.move(piece) assert board.squares[48].contains?(piece) end end
  • time describe Event do it quot;is not happening before the start timequot; do now = Time.now start = now + 1 Time.stub(:now).and_return now event = Event.new(:start => start) event.should_not be_happening end end
  • isolation from external dependencies
  • network access Database Interface Database Subject Network Internets Interface
  • network access def test_successful_purchase_sends_shipping_message ActiveMerchant::Billing::Base.mode = :test gateway = ActiveMerchant::Billing::TrustCommerceGateway.new( :login => 'TestMerchant', :password => 'password' ) item = stub() messenger = mock() messenger.expects(:ship).with(item) purchase = Purchase.new(gateway, item, credit_card, messenger) purchase.finalize end
  • network access Stub Database Code Subject Example Stub Network
  • network access def test_successful_purchase_sends_shipping_message gateway = stub() gateway.stubs(:authorize).returns( ActiveMerchant::Billing::Response.new(true, quot;ignorequot;) ) item = stub() messenger = mock() messenger.expects(:ship).with(item) purchase = Purchase.new(gateway, item, credit_card, messenger) purchase.finalize end
  • polymorphic collaborators
  • strategies describe Employee do it quot;delegates pay() to payment strategyquot; do payment_strategy = mock() employee = Employee.new(payment_strategy) payment_strategy.expects(:pay) employee.pay end end
  • mixins/plugins describe AgeIdentifiable do describe quot;#can_vote?quot; do it quot;raises if including does not respond to birthdatequot; do object = Object.new object.extend AgeIdentifiable expect { object.can_vote? }.to raise_error( /must supply a birthdate/ ) end it quot;returns true if birthdate == 18 years agoquot; do object = Object.new stub(object).birthdate {18.years.ago.to_date} object.extend AgeIdentifiable object.can_vote?.should be(true) end end end
  • when are message expectations helpful?
  • side effects describe Statement do it quot;logs a message when printedquot; do customer = stub(quot;customerquot;) customer.stub(:name).and_return('Joe Customer') logger = mock(quot;loggerquot;) statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Joe Customer/) statement.print end end
  • caching describe ZipCode do it quot;should only validate oncequot; do validator = mock() zipcode = ZipCode.new(quot;02134quot;, validator) validator.should_receive(:valid?).with(quot;02134quot;).once. and_return(true) zipcode.valid? zipcode.valid? end end
  • interface discovery describe quot;thing I'm working onquot; do it quot;does something with some assistancequot; do thing_i_need = mock() thing_i_am_working_on = ThingIAmWorkingOn.new(thing_i_need) thing_i_need.should_receive(:help_me).and_return('what I need') thing_i_am_working_on.do_something_complicated end end
  • isolation testing
  • specifying/testing individual objects in isolation
  • good fit with lots of little objects
  • all of these concepts apply to the non-rails specific parts of our rails apps
  • isolation testing the rails-specific parts of our applicationss
  • V M C
  • View Controller Model
  • Browser Router View Controller Model Database
  • rails testing ๏ unit tests ๏ functional tests ๏ integration tests
  • rails unit tests ๏ model classes (repositories) ๏ model objects ๏ database
  • rails functional tests ๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers
  • rails functional tests ๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers
  • rails functional tests ๏ model classes (repositories) ๏ model objects !DRY ๏ database ๏ views ๏ controllers
  • rails integration tests ๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers ๏ routing/sessions
  • rails integration tests ๏ model classes (repositories) ๏ model objects ๏ database !DRY ๏ views ๏ controllers ๏ routing/sessions
  • the BDD approach
  • inherited from XP
  • customer specs developer specs
  • rails integration tests + webrat shoulda, context, micronaut, etc
  • customer specs are implemented as end to end tests
  • developer specs are implemented as isolation tests
  • mocking and stubbing with rails
  • partials in view specs describe quot;/registrations/new.html.erbquot; do before(:each) do template.stub(:render).with(:partial => anything) end it quot;renders the registration navigationquot; do template.should_receive(:render).with(:partial => 'nav') render end it quot;renders the registration form quot; do template.should_receive(:render).with(:partial => 'form') render end end
  • conditional branches in controller specs describe quot;POST createquot; do describe quot;with valid attributesquot; do it quot;redirects to list of registrationsquot; do registration = stub_model(Registration) Registration.stub(:new).and_return(registration) registration.stub(:save!).and_return(true) post 'create' response.should redirect_to(registrations_path) end end end
  • conditional branches in controller specs describe quot;POST createquot; do describe quot;with invalid attributesquot; do it quot;re-renders the new formquot; do registration = stub_model(Registration) Registration.stub(:new).and_return(registration) registration.stub(:save!).and_raise( ActiveRecord::RecordInvalid.new(registration)) post 'create' response.should render_template('new') end end end
  • conditional branches in controller specs describe quot;POST createquot; do describe quot;with invalid attributesquot; do it quot;assigns the registrationquot; do registration = stub_model(Registration) Registration.stub(:new).and_return(registration) registration.stub(:save!).and_raise( ActiveRecord::RecordInvalid.new(registration)) post 'create' assigns[:registration].should equal(registration) end end end
  • shave a few lines but leave a little stubble http://github.com/dchelimsky/stubble
  • stubble describe quot;POST createquot; do describe quot;with valid attributesquot; do it quot;redirects to list of registrationsquot; do stubbing(Registration) do post 'create' response.should redirect_to(registrations_path) end end end end
  • stubble describe quot;POST createquot; do describe quot;with invalid attributesquot; do it quot;re-renders the new formquot; do stubbing(Registration, :as => :invalid) do post 'create' response.should render_template('new') end end it quot;assigns the registrationquot; do stubbing(Registration, :as => :invalid) do |registration| post 'create' assigns[:registration].should equal(registration) end end end end
  • chains describe UsersController do it quot;GET 'best_friend'quot; do member = stub_model(User) friends = stub() friend = stub_model(User) User.stub(:find).and_return(member) member.stub(:friends).and_return(friends) friends.stub(:favorite).and_return(friend) get :best_friend, :id => '37' assigns[:friend].should equal(friend) end end
  • chains describe UsersController do it quot;GET 'best_friend'quot; do friend = stub_model(User) User.stub_chain(:find, :friends, :favorite). and_return(friend) get :best_friend, :id => '37' assigns[:friend].should equal(friend) end end
  • guidlines, pitfalls, and common concerns
  • focus on roles Mock Roles, not Objects http://www.jmock.org/oopsla2004.pdf
  • keep things simple
  • avoid tight coupling
  • complex setup is a red flag for design issues
  • don’t stub/mock the object you’re testing
  • impedes refactoring
  • :refactoring => <<-DEFINITION Improving design without changing behaviour DEFINITION
  • what is behaviour?
  • false positives describe RegistrationsController do describe quot;GET 'pending'quot; do it quot;finds the pending registrationsquot; do pending_registration = stub_model(Registration) Registration.should_receive(:pending). and_return([pending_registration]) get 'pending' assigns[:registrations].should == [pending_registration] end end end class RegistrationsController < ApplicationController def pending @registrations = Registration.pending end end
  • false positives describe RegistrationsController do describe quot;GET 'pending'quot; do it quot;finds the pending registrationsquot; do pending_registration = stub_model(Registration) Registration.should_receive(:pending). and_return([pending_registration]) get 'pending' assigns[:registrations].should == [pending_registration] end end end class RegistrationsController < ApplicationController def pending @registrations = Registration.pending end end
  • false positives describe Registration do describe quot;#pendingquot; do it quot;finds pending registrationsquot; do Registration.create! Registration.create!(:pending => true) Registration.pending.should have(1).item end end end class Registration < ActiveRecord::Base named_scope :pending, :conditions => {:pending => true} end
  • false positives describe Registration do describe quot;#pendingquot; do it quot;finds pending registrationsquot; do Registration.create! Registration.create!(:pending => true) Registration.pending.should have(1).item end end end class Registration < ActiveRecord::Base named_scope :pending, :conditions => {:pending => true} end
  • false positives describe Registration do describe quot;#pendingquot; do it quot;finds pending registrationsquot; do Registration.create! Registration.create!(:pending => true) Registration.pending_confirmation.should have(1).item end end end class Registration < ActiveRecord::Base named_scope :pending_confirmation, :conditions => {:pending => true} end
  • false positives describe Registration do describe quot;#pendingquot; do it quot;finds pending registrationsquot; do Registration.create! Registration.create!(:pending => true) Registration.pending_confirmation.should have(1).item end end end class Registration < ActiveRecord::Base named_scope :pending_confirmation, :conditions => {:pending => true} end
  • cucumber
  • http://pragprog.com/titles/achbd/the-rspec-book http://xunitpatterns.com/ growing object-oriented Mock Roles, not Objects software, guided by tests http://www.jmock.org/oopsla2004.pdf http://www.mockobjects.com/book/
  • http://blog.davidchelimsky.net/ http://www.articulatedman.com/ http://rspec.info/ http://cukes.info/ http://pragprog.com/titles/achbd/the-rspec-book
  • ruby frameworks
  • rspec-mocks http://github.com/dchelimsky/rspec
  • mocha http://github.com/floehopper/mocha
  • flexmock http://github.com/jimweirich/flexmock
  • rr http://github.com/btakita/rr
  • not a mock http://github.com/notahat/not_a_mock