You are on page 1of 126

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 "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"


end
end
test stub
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"


end
end
test stub
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"


end
end
test stub
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"


end
end
mock object
describe 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
end
end
mock object
describe 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
end
end
mock object
describe 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
end
end
mock object
describe 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
end
end
mock object
describe 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
end
end
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
end
end
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
end
end
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
end
end
method stub
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
end
end
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
end
end
message expectation
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
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end
message
class Statement expectation
def header
"Statement for #{@customer.name}"
end
end
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"


end
end
bound to
class Statement implementation
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement
def header
"Statement for #{@customer.name}"
end
end
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"


end
end

class Statement ????


def header
"Statement for #{@customer.name}"
end
end
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"


end
end
message
class Statement expectation
def header
"Statement for #{@customer.name}"
end
end
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"


end
end
bound to
class Statement implementation
def header
"Statement for #{@customer.name}"
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 "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
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, "ignore")
)
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 "delegates pay() to payment strategy" do
payment_strategy = mock()
employee = Employee.new(payment_strategy)

payment_strategy.expects(:pay)

employee.pay
end
end
mixins/plugins
describe 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
end
end
when are
message expectations
helpful?
side effects
describe 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
end
end
caching
describe 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?
end
end
interface discovery
describe "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
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
M V

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
๏ database
!DRY
๏ views
๏ controllers
rails integration tests
๏ model classes (repositories)
๏ model objects
๏ database
๏ views
๏ controllers
๏ routing/sessions
rails integration tests
๏ model classes (repositories)
๏ model objects
๏ database
๏ views
!DRY
๏ 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 "/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
end
end
conditional branches in
controller 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
end
end
conditional branches in
controller specs
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
end
end
conditional branches in
controller 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
end
end
shave a few lines but
leave a little stubble

http://github.com/dchelimsky/stubble
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
end
end
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
end
end
chains
describe 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)
end
end
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)
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 "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
end
end

class RegistrationsController < ApplicationController


def pending
@registrations = Registration.pending
end
end
false positives
describe 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
end
end

class RegistrationsController < ApplicationController


def pending
@registrations = Registration.pending
end
end
false positives
describe Registration do
describe "#pending" do
it "finds pending registrations" 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 "#pending" do
it "finds pending registrations" 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 "#pending" do
it "finds pending registrations" 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 "#pending" do
it "finds pending registrations" 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

You might also like