126
Don’t Mock Yourself Out David Chelimsky Articulated Man, Inc

David Chelimsky Articulated Man, Incassets.en.oreilly.com/1/event/24/Don't Mock Yourself Out... · Don’t Mock Yourself Out David Chelimsky Articulated Man, Inc. . Classical and

Embed Size (px)

Citation preview

Don’t MockYourself Out

David ChelimskyArticulated Man, Inc

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

not a mock

http://github.com/notahat/not_a_mock