Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
Don’t MockYourself Out
David ChelimskyArticulated 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 einred 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 stubdescribe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
test stubdescribe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
test stubdescribe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
test stubdescribe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
mock objectdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
mock objectdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
mock objectdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
mock objectdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
mock objectdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
method level concepts
describe Statement do it "logs a message when printed" 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 endend
describe Statement do it "logs a message when printed" 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 endend
describe Statement do it "logs a message when printed" 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 endend
method stubdescribe Statement do it "logs a message when printed" 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 endend
describe Statement do it "logs a message when printed" 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 endend
message expectationdescribe Statement do it "logs a message when printed" 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 endend
things aren’t always as they seem
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
messageexpectation
bound to implementation
describe Statement do it "uses the customer name in the header" do customer = mock("customer") statement = Statement.new(customer) customer.should_receive(:name).and_return('Joe Customer')
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
????
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
messageexpectation
describe Statement do it "uses the customer name in the header" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') statement = Statement.new(customer)
statement.header.should == "Statement for Joe Customer" endend
class Statement def header "Statement for #{@customer.name}" endend
bound to implementation
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 aremethod stubs
helpful?
isolation fromnon-determinism
random values
random valuesclass 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) endend
time
describe Event do it "is not happening before the start time" do now = Time.now start = now + 1 Time.stub(:now).and_return now event = Event.new(:start => start) event.should_not be_happening endend
isolation fromexternal dependencies
network access
Subject
DatabaseDatabase Interface
NetworkInterface Internets
network accessdef 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.finalizeend
network access
Subject
StubDatabase
StubNetwork
CodeExample
network access
def test_successful_purchase_sends_shipping_message gateway = stub() gateway.stubs(:authorize).returns( ActiveMerchant::Billing::Response.new(true, "ignore") ) item = stub() messenger = mock()
messenger.expects(:ship).with(item) purchase = Purchase.new(gateway, item, credit_card, messenger) purchase.finalizeend
polymorphic collaborators
strategies
describe Employee do it "delegates pay() to payment strategy" do payment_strategy = mock() employee = Employee.new(payment_strategy)
payment_strategy.expects(:pay) employee.pay endend
mixins/pluginsdescribe AgeIdentifiable do describe "#can_vote?" do it "raises if including does not respond to birthdate" do object = Object.new object.extend AgeIdentifiable expect { object.can_vote? }.to raise_error( /must supply a birthdate/ ) end
it "returns true if birthdate == 18 years ago" do object = Object.new stub(object).birthdate {18.years.ago.to_date} object.extend AgeIdentifiable object.can_vote?.should be(true) end endend
when aremessage expectations
helpful?
side effectsdescribe Statement do it "logs a message when printed" do customer = stub("customer") customer.stub(:name).and_return('Joe Customer') logger = mock("logger") statement = Statement.new(customer, logger)
logger.should_receive(:log).with(/Joe Customer/) statement.print endend
cachingdescribe ZipCode do it "should only validate once" do validator = mock() zipcode = ZipCode.new("02134", validator) validator.should_receive(:valid?).with("02134").once. and_return(true) zipcode.valid? zipcode.valid? endend
interface discoverydescribe "thing I'm working on" do it "does something with some assistance" 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 endend
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
M
C
V
ViewController
Model
ViewController
Model
BrowserRouter
Database
rails testing๏ unit tests
๏ functional tests
๏ integration tests
rails unit tests๏ model classes (repositories)
๏ model objects
๏ database
๏ 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
๏ database
๏ views
๏ controllers
rails functional tests
!DRY
rails integration tests๏ model classes (repositories)
๏ model objects
๏ database
๏ views
๏ controllers
๏ routing/sessions
rails integration tests๏ model classes (repositories)
๏ model objects
๏ database
๏ views
๏ controllers
๏ routing/sessions
!DRY
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 stubbingwith rails
partials in view specsdescribe "/registrations/new.html.erb" do before(:each) do template.stub(:render).with(:partial => anything) end it "renders the registration navigation" do template.should_receive(:render).with(:partial => 'nav') render end it "renders the registration form " do template.should_receive(:render).with(:partial => 'form') render endend
conditional branches incontroller specs
describe "POST create" do describe "with valid attributes" do it "redirects to list of registrations" 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 endend
describe "POST create" do describe "with invalid attributes" do it "re-renders the new form" 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 endend
conditional branches incontroller specs
describe "POST create" do describe "with invalid attributes" do it "assigns the registration" 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 endend
conditional branches incontroller specs
shave a few lines but leave a little stubble
http://github.com/dchelimsky/stubble
describe "POST create" do describe "with valid attributes" do it "redirects to list of registrations" do stubbing(Registration) do post 'create' response.should redirect_to(registrations_path) end end endend
stubble
describe "POST create" do describe "with invalid attributes" do it "re-renders the new form" do stubbing(Registration, :as => :invalid) do post 'create' response.should render_template('new') end end it "assigns the registration" do stubbing(Registration, :as => :invalid) do |registration| post 'create' assigns[:registration].should equal(registration) end end endend
stubble
chainsdescribe UsersController do it "GET 'best_friend'" 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) endend
chains
describe UsersController do it "GET 'best_friend'" do friend = stub_model(User) User.stub_chain(:find, :friends, :favorite). and_return(friend) get :best_friend, :id => '37' assigns[:friend].should equal(friend) endend
guidlines, pitfalls, andcommon concerns
focus on roles
http://www.jmock.org/oopsla2004.pdf
Mock Roles, not Objects
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 positivesdescribe RegistrationsController do describe "GET 'pending'" do it "finds the pending registrations" do pending_registration = stub_model(Registration) Registration.should_receive(:pending). and_return([pending_registration]) get 'pending' assigns[:registrations].should == [pending_registration] end endend
class RegistrationsController < ApplicationController def pending @registrations = Registration.pending endend
false positivesdescribe RegistrationsController do describe "GET 'pending'" do it "finds the pending registrations" do pending_registration = stub_model(Registration) Registration.should_receive(:pending). and_return([pending_registration]) get 'pending' assigns[:registrations].should == [pending_registration] end endend
class RegistrationsController < ApplicationController def pending @registrations = Registration.pending endend
false positivesdescribe Registration do describe "#pending" do it "finds pending registrations" do Registration.create! Registration.create!(:pending => true) Registration.pending.should have(1).item end endend
class Registration < ActiveRecord::Base named_scope :pending, :conditions => {:pending => true}end
false positivesdescribe Registration do describe "#pending" do it "finds pending registrations" do Registration.create! Registration.create!(:pending => true) Registration.pending.should have(1).item end endend
class Registration < ActiveRecord::Base named_scope :pending, :conditions => {:pending => true}end
false positivesdescribe Registration do describe "#pending" do it "finds pending registrations" do Registration.create! Registration.create!(:pending => true) Registration.pending_confirmation.should have(1).item end endend
class Registration < ActiveRecord::Base named_scope :pending_confirmation, :conditions => {:pending => true}end
false positivesdescribe Registration do describe "#pending" do it "finds pending registrations" do Registration.create! Registration.create!(:pending => true) Registration.pending_confirmation.should have(1).item end endend
class Registration < ActiveRecord::Base named_scope :pending_confirmation, :conditions => {:pending => true}end
cucumber
http://pragprog.com/titles/achbd/the-rspec-book
http://www.jmock.org/oopsla2004.pdf http://www.mockobjects.com/book/
Mock Roles, not Objectsgrowing object-orientedsoftware, guided by tests
http://xunitpatterns.com/
http://pragprog.com/titles/achbd/the-rspec-book
http://blog.davidchelimsky.net/http://www.articulatedman.com/
http://rspec.info/http://cukes.info/
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