Dont Mock Yourself Out David Chelimsky Articulated Man, Inc - - PowerPoint PPT Presentation

don t mock yourself out
SMART_READER_LITE
LIVE PREVIEW

Dont Mock Yourself Out David Chelimsky Articulated Man, Inc - - PowerPoint PPT Presentation

Dont 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


slide-1
SLIDE 1

Don’t Mock Yourself Out

David Chelimsky Articulated Man, Inc

slide-2
SLIDE 2
slide-3
SLIDE 3

http://martinfowler.com/articles/mocksArentStubs.html

slide-4
SLIDE 4

Classical and Mockist Testing

slide-5
SLIDE 5

Classical and Mockist Testing

slide-6
SLIDE 6

Classical and Mockist Testing

slide-7
SLIDE 7

classicist mockist

slide-8
SLIDE 8

merbist railsist

slide-9
SLIDE 9

rspecist testunitist

slide-10
SLIDE 10
slide-11
SLIDE 11

ist bin ein red herring

slide-12
SLIDE 12

The big issue here is when to use a mock

http://martinfowler.com/articles/mocksArentStubs.html

slide-13
SLIDE 13

agenda

๏ overview of stubs and mocks ๏ mocks/stubs applied to rails ๏ guidelines and pitfalls ๏ questions

slide-14
SLIDE 14

test double

slide-15
SLIDE 15

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

slide-16
SLIDE 16

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

slide-17
SLIDE 17

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

slide-18
SLIDE 18

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

slide-19
SLIDE 19

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

slide-20
SLIDE 20

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

slide-21
SLIDE 21

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

slide-22
SLIDE 22

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

slide-23
SLIDE 23

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

slide-24
SLIDE 24

method level concepts

slide-25
SLIDE 25

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

slide-26
SLIDE 26

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

slide-27
SLIDE 27

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

slide-28
SLIDE 28

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

slide-29
SLIDE 29

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

slide-30
SLIDE 30

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

slide-31
SLIDE 31

things aren’t always as they seem

slide-32
SLIDE 32

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

slide-33
SLIDE 33

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

slide-34
SLIDE 34

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

slide-35
SLIDE 35

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

slide-36
SLIDE 36

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

message expectation

slide-37
SLIDE 37

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

slide-38
SLIDE 38

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

slide-39
SLIDE 39

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

slide-40
SLIDE 40

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

slide-41
SLIDE 41

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

slide-42
SLIDE 42

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

????

slide-43
SLIDE 43

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

message expectation

slide-44
SLIDE 44

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

bound to implementation

slide-45
SLIDE 45

stubs are often used like mocks

slide-46
SLIDE 46

mocks are often used like stubs

slide-47
SLIDE 47

we verify stubs by checking state after an action

slide-48
SLIDE 48

we tell mocks to verify interactions

slide-49
SLIDE 49

sometimes stubs just make the system run

slide-50
SLIDE 50

when are method stubs helpful?

slide-51
SLIDE 51

isolation from non-determinism

slide-52
SLIDE 52

random values

slide-53
SLIDE 53

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

slide-54
SLIDE 54

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

slide-55
SLIDE 55

isolation from external dependencies

slide-56
SLIDE 56

network access

Subject

Database

Database Interface Network Interface Internets

slide-57
SLIDE 57

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

slide-58
SLIDE 58

network access

Subject Stub Database Stub Network Code Example

slide-59
SLIDE 59

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

slide-60
SLIDE 60

polymorphic collaborators

slide-61
SLIDE 61

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

slide-62
SLIDE 62

mixins/plugins

describe AgeIdentifiable do describe "#can_vote?" do it "raises if including does not respond to birthdate" do

  • bject = Object.new
  • bject.extend AgeIdentifiable

expect { object.can_vote? }.to raise_error( /must supply a birthdate/ ) end it "returns true if birthdate == 18 years ago" do

  • bject = Object.new

stub(object).birthdate {18.years.ago.to_date}

  • bject.extend AgeIdentifiable
  • bject.can_vote?.should be(true)

end end end

slide-63
SLIDE 63

when are message expectations helpful?

slide-64
SLIDE 64

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

slide-65
SLIDE 65

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

slide-66
SLIDE 66

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

slide-67
SLIDE 67

isolation testing

slide-68
SLIDE 68

specifying/testing individual

  • bjects in isolation
slide-69
SLIDE 69

good fit with lots of little objects

slide-70
SLIDE 70
slide-71
SLIDE 71

all of these concepts apply to the non-rails specific parts of

  • ur rails apps
slide-72
SLIDE 72

isolation testing the rails-specific parts

  • f our applicationss
slide-73
SLIDE 73
slide-74
SLIDE 74

M C V

slide-75
SLIDE 75

View Controller Model

slide-76
SLIDE 76

View Controller Model Browser Router Database

slide-77
SLIDE 77

rails testing

๏ unit tests ๏ functional tests ๏ integration tests

slide-78
SLIDE 78

rails unit tests

๏ model classes (repositories) ๏ model objects ๏ database

slide-79
SLIDE 79

๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers

rails functional tests

slide-80
SLIDE 80

๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers

rails functional tests

slide-81
SLIDE 81

๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers

rails functional tests

!DRY

slide-82
SLIDE 82

rails integration tests

๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers ๏ routing/sessions

slide-83
SLIDE 83

rails integration tests

๏ model classes (repositories) ๏ model objects ๏ database ๏ views ๏ controllers ๏ routing/sessions

!DRY

slide-84
SLIDE 84

the BDD approach

slide-85
SLIDE 85

inherited from XP

slide-86
SLIDE 86

customer specs developer specs

slide-87
SLIDE 87
slide-88
SLIDE 88

rails integration tests + webrat shoulda, context, micronaut, etc

slide-89
SLIDE 89

customer specs are implemented as end to end tests

slide-90
SLIDE 90

developer specs are implemented as isolation tests

slide-91
SLIDE 91

mocking and stubbing with rails

slide-92
SLIDE 92

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

slide-93
SLIDE 93

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

slide-94
SLIDE 94

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

slide-95
SLIDE 95

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

conditional branches in controller specs

slide-96
SLIDE 96

shave a few lines but leave a little stubble

http://github.com/dchelimsky/stubble

slide-97
SLIDE 97

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

slide-98
SLIDE 98

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

stubble

slide-99
SLIDE 99

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

slide-100
SLIDE 100

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

slide-101
SLIDE 101

guidlines, pitfalls, and common concerns

slide-102
SLIDE 102

focus on roles

http://www.jmock.org/oopsla2004.pdf

Mock Roles, not Objects

slide-103
SLIDE 103

keep things simple

slide-104
SLIDE 104

avoid tight coupling

slide-105
SLIDE 105

complex setup is a red flag for design issues

slide-106
SLIDE 106

don’t stub/mock the

  • bject you’re testing
slide-107
SLIDE 107

impedes refactoring

slide-108
SLIDE 108

:refactoring => <<-DEFINITION Improving design without changing behaviour DEFINITION

slide-109
SLIDE 109

what is behaviour?

slide-110
SLIDE 110
slide-111
SLIDE 111

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

slide-112
SLIDE 112

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

slide-113
SLIDE 113

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

slide-114
SLIDE 114

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

slide-115
SLIDE 115

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

slide-116
SLIDE 116

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

slide-117
SLIDE 117

cucumber

slide-118
SLIDE 118
slide-119
SLIDE 119

http://pragprog.com/titles/achbd/the-rspec-book

http://www.jmock.org/oopsla2004.pdf http://www.mockobjects.com/book/

Mock Roles, not Objects growing object-oriented software, guided by tests

http://xunitpatterns.com/

slide-120
SLIDE 120

http://pragprog.com/titles/achbd/the-rspec-book http://blog.davidchelimsky.net/ http://www.articulatedman.com/ http://rspec.info/ http://cukes.info/

slide-121
SLIDE 121

ruby frameworks

slide-122
SLIDE 122

rspec-mocks

http://github.com/dchelimsky/rspec

slide-123
SLIDE 123

mocha

http://github.com/floehopper/mocha

slide-124
SLIDE 124

flexmock

http://github.com/jimweirich/flexmock

slide-125
SLIDE 125

rr

http://github.com/btakita/rr

slide-126
SLIDE 126

not a mock

http://github.com/notahat/not_a_mock