Slide 1

Slide 1 text

Gregory Moeck Why You Don’t Get Mock Objects @gregmoeck Thursday, September 29, 11

Slide 2

Slide 2 text

Introduction Thursday, September 29, 11

Slide 3

Slide 3 text

Me Thursday, September 29, 11

Slide 4

Slide 4 text

I Am Not A Primary Teacher Thursday, September 29, 11

Slide 5

Slide 5 text

I Am A Secondary Teacher Thursday, September 29, 11

Slide 6

Slide 6 text

“[The secondary teacher] should regard himself as learning from the masters along with his [students]. He should not act as if he were a primary teacher, using a great book as if it were just another textbook of the sort one of his colleagues might write. He should not masquerade as one who knows and can teach by virtue of his original discoveries...The primary sources of his own knowledge should be the primary sources of learning for his students, and such a teacher functions honestly only if he does not aggrandize himself by coming between the great books and their ... readers. He should not “come between” as a nonconductor, but he should come between as a mediator-as one who helps the less competent make more effective contacts with the best minds. Mortimer Adler, How to Read a Book, p. 60 Thursday, September 29, 11

Slide 7

Slide 7 text

Thursday, September 29, 11

Slide 8

Slide 8 text

Mock Objects Thursday, September 29, 11

Slide 9

Slide 9 text

The Ruby Community + Mock Objects = ? Thursday, September 29, 11

Slide 10

Slide 10 text

Love? Thursday, September 29, 11

Slide 11

Slide 11 text

Love? X Thursday, September 29, 11

Slide 12

Slide 12 text

Mock Objects Have Been Called: Thursday, September 29, 11

Slide 13

Slide 13 text

“Wastes Of Time Thursday, September 29, 11

Slide 14

Slide 14 text

“ Scams Thursday, September 29, 11

Slide 15

Slide 15 text

“ Testing Heresies Thursday, September 29, 11

Slide 16

Slide 16 text

Why? Thursday, September 29, 11

Slide 17

Slide 17 text

1. Duplicate Implementation Thursday, September 29, 11

Slide 18

Slide 18 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Francis Hwang’s 2008 RubyConf talk “Testing Heresies”. Thursday, September 29, 11

Slide 19

Slide 19 text

class ShowController def index @shows = Show.all( :select => “id, name, video_count”, :conditions =>“shows.video_count > 0” ) end end Thursday, September 29, 11

Slide 20

Slide 20 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Thursday, September 29, 11

Slide 21

Slide 21 text

class ShowController def index @shows = Show.all( :select => “id, name, video_count”, :conditions =>“shows.video_count > 0” ) end end Thursday, September 29, 11

Slide 22

Slide 22 text

2. Lead To Brittle Tests That Do A Poor Job Thursday, September 29, 11

Slide 23

Slide 23 text

module Codebreaker describe Game do describe "#start" do it "sends a welcome message" do output = double('output') game = Game.new(output) output.should_receive(:puts) .with('Welcome to Codebreaker!') game.start end end end end Nick Gauthier in his blog post “Everything that is wrong with Mock Objects” Thursday, September 29, 11

Slide 24

Slide 24 text

module Codebreaker class Game def initialize(output) @output = output end def start @output.puts(“Welcome to Codebreaker”) end end end Thursday, September 29, 11

Slide 25

Slide 25 text

Refactor To The Following: Thursday, September 29, 11

Slide 26

Slide 26 text

module Codebreaker class Game def initialize(output) @output = output end def start @output.writes(“Welcome to Codebreaker”) end end end Thursday, September 29, 11

Slide 27

Slide 27 text

The Tests Fail Thursday, September 29, 11

Slide 28

Slide 28 text

The Output Does Not Change Thursday, September 29, 11

Slide 29

Slide 29 text

QED Thursday, September 29, 11

Slide 30

Slide 30 text

Mocks Suck! Thursday, September 29, 11

Slide 31

Slide 31 text

Maybe Thursday, September 29, 11

Slide 32

Slide 32 text

Or Maybe You Just Don’t Understand Them Tell story about how I was going to give a presentation on testing, and though about slamming Mock Objects. Thursday, September 29, 11

Slide 33

Slide 33 text

Why Mocks? Thursday, September 29, 11

Slide 34

Slide 34 text

Mock Objects + Procedural Programming = Bad Idea Thursday, September 29, 11

Slide 35

Slide 35 text

Mocks Are Not Stubs Thursday, September 29, 11

Slide 36

Slide 36 text

Mocks Assert On Messages Thursday, September 29, 11

Slide 37

Slide 37 text

Stubs Return Values Thursday, September 29, 11

Slide 38

Slide 38 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Thursday, September 29, 11

Slide 39

Slide 39 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Thursday, September 29, 11

Slide 40

Slide 40 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Thursday, September 29, 11

Slide 41

Slide 41 text

describe ShowController, "index" do context "when a TV show has no public videos" do it “should not display any shows” do Show.should_receive(:all) .with(:select => “id, name, video_count”, :conditions => “shows.video_count > 0”) .and_return([]) get ‘index’ response.body.should_not(match(/ #{@show.name}) end end end Thursday, September 29, 11

Slide 42

Slide 42 text

Mock Objects + OOP = Good Idea Thursday, September 29, 11

Slide 43

Slide 43 text

Key Idea In OOP: Thursday, September 29, 11

Slide 44

Slide 44 text

Objects Tell Objects To Do Things Thursday, September 29, 11

Slide 45

Slide 45 text

“The big idea is “messaging” -- that is what the kernal of Smalltalk/Squeak is all about...The key in making great and growable systems is much more to design how its modules communicate rather than what their internal behaviors should be. Alan Kay, Email Message Sent to the Squeak Mailing List. http://lists.squeakfoundation.org/pipermail/squeak-dev/1998-October/017019.html Thursday, September 29, 11

Slide 46

Slide 46 text

Thursday, September 29, 11

Slide 47

Slide 47 text

Thursday, September 29, 11

Slide 48

Slide 48 text

Change The Behavior Of The System By Composing Objects Thursday, September 29, 11

Slide 49

Slide 49 text

(Contrived) Example: Thursday, September 29, 11

Slide 50

Slide 50 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Thursday, September 29, 11

Slide 51

Slide 51 text

Imagine You Want To Save The Requests Before Sending Them Thursday, September 29, 11

Slide 52

Slide 52 text

Procedural Programming: Add Code To The Method Thursday, September 29, 11

Slide 53

Slide 53 text

OO Programming: Change Object Composition Thursday, September 29, 11

Slide 54

Slide 54 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Persist Ticket Requests Reserve Tickets Thursday, September 29, 11

Slide 55

Slide 55 text

What Would The Code For The Interface Look Like? Thursday, September 29, 11

Slide 56

Slide 56 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 57

Slide 57 text

Notice Two Things: Thursday, September 29, 11

Slide 58

Slide 58 text

1. Follows Tell, Don’t Ask Principle Thursday, September 29, 11

Slide 59

Slide 59 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 60

Slide 60 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 61

Slide 61 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 62

Slide 62 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 63

Slide 63 text

2. Hides It’s Internal State Thursday, September 29, 11

Slide 64

Slide 64 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 65

Slide 65 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 66

Slide 66 text

How Can You Assert On State? Thursday, September 29, 11

Slide 67

Slide 67 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = "" end def number_pressed(number) @current_display += number.to_s end def delete_pressed @current_display.chop! end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 68

Slide 68 text

You Would Have To Add Getters Just For Tests Thursday, September 29, 11

Slide 69

Slide 69 text

Bad Idea! Thursday, September 29, 11

Slide 70

Slide 70 text

How Then Do You Test? Thursday, September 29, 11

Slide 71

Slide 71 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Remember what Alan Kay said, it’s all about the messages Thursday, September 29, 11

Slide 72

Slide 72 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Thursday, September 29, 11

Slide 73

Slide 73 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Assert On The Message Thursday, September 29, 11

Slide 74

Slide 74 text

How? Thursday, September 29, 11

Slide 75

Slide 75 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Thursday, September 29, 11

Slide 76

Slide 76 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Fake Object Reserve Tickets Instead of talking to a real object, talk to a fake object whose job it is to record all of the messages that it receives, then you can assert on that. Thursday, September 29, 11

Slide 77

Slide 77 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock Object Reserve Tickets Thursday, September 29, 11

Slide 78

Slide 78 text

describe TicketMachineInterface do it "reserves the number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end Thursday, September 29, 11

Slide 79

Slide 79 text

describe TicketMachineInterface do it "reserves the number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end Thursday, September 29, 11

Slide 80

Slide 80 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock Object Reserve Tickets Thursday, September 29, 11

Slide 81

Slide 81 text

describe TicketMachineInterface do it "reserves the number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end Thursday, September 29, 11

Slide 82

Slide 82 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock Object Reserve Tickets Thursday, September 29, 11

Slide 83

Slide 83 text

describe TicketMachineInterface do it "reserves the number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end Thursday, September 29, 11

Slide 84

Slide 84 text

describe TicketMachineInterface do it "reserves the number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end Thursday, September 29, 11

Slide 85

Slide 85 text

Key Idea Thursday, September 29, 11

Slide 86

Slide 86 text

In OOP, Behavior Is Found In Messages Thursday, September 29, 11

Slide 87

Slide 87 text

Mocks Assert On Messages Talk about how tools are made to be used a certain way. A screwdriver might work as a hammer if you have no other tool, but it works better as a screwdriver. Thursday, September 29, 11

Slide 88

Slide 88 text

Key Mocking Rules Thursday, September 29, 11

Slide 89

Slide 89 text

Mock Roles, Not Objects Thursday, September 29, 11

Slide 90

Slide 90 text

Hhttp://www.jmock.org/oopsla2004.pdf Thursday, September 29, 11

Slide 91

Slide 91 text

Wanting To Mock Concrete Objects Is A Design Smell Thursday, September 29, 11

Slide 92

Slide 92 text

Well Designed Objects Don’t Know Who They’re Talking To Thursday, September 29, 11

Slide 93

Slide 93 text

Why? Thursday, September 29, 11

Slide 94

Slide 94 text

Because Who They’re Talking To Can Change Thursday, September 29, 11

Slide 95

Slide 95 text

They Should Only Know The ROLE Their Collaborator Is Playing Thursday, September 29, 11

Slide 96

Slide 96 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Thursday, September 29, 11

Slide 97

Slide 97 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Ticket Machine Request Handler Thursday, September 29, 11

Slide 98

Slide 98 text

Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Persist Ticket Requests Reserve Tickets Ticket Machine Request Handler Thursday, September 29, 11

Slide 99

Slide 99 text

When Mocking Roles, TDD Becomes A Design Process Thursday, September 29, 11

Slide 100

Slide 100 text

class TicketMachineInterface def number_pressed(number) end def delete_pressed end def submit_request end end I ask myself what is a scenario that describes the domain logic for this implementation of this role. Thursday, September 29, 11

Slide 101

Slide 101 text

describe TicketMachineInterface do it "reserves the correct number of tickets when a number is pressed two times before submitting" do machine = TicketMachineInterface.new machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end This is the scenario, so I then ask myself what is the responsibility of THIS object, and what is not. I decide that this object is not responsible for handling the request, just for sending it. Thursday, September 29, 11

Slide 102

Slide 102 text

describe TicketMachineInterface do it "reserves the correct number of tickets inputted when the user submits a request" do request_handler = double('request_handler') request_handler.should_receive(:reserve).with(55) machine = TicketMachineInterface.new(request_handler) machine.number_pressed(5) machine.number_pressed(5) machine.submit_request end end So I create a mock object for the role that this object’s peer will play. Thursday, September 29, 11

Slide 103

Slide 103 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler end def number_pressed(number) end def delete_pressed end def submit_request @request_handler.reserve(@current_display) end end Thursday, September 29, 11

Slide 104

Slide 104 text

class TicketMachineInterface def initialize(request_handler) @request_handler = request_handler @current_display = ‘’ end def number_pressed(number) @current_display += number.to_s end def delete_pressed end def submit_request @request_handler.reserve(@current_display.to_i) end end Thursday, September 29, 11

Slide 105

Slide 105 text

Only Mock Types You Own Thursday, September 29, 11

Slide 106

Slide 106 text

http://www.mockobjects.com/2008/11/only-mock-types-you- own-revisited.html http://www.jmock.org/oopsla2004.pdf Thursday, September 29, 11

Slide 107

Slide 107 text

If You Don’t Own The API, There Is No Design Feedback Thursday, September 29, 11

Slide 108

Slide 108 text

Instead Of Mocking A Role, Your Mocking An Implementation Thursday, September 29, 11

Slide 109

Slide 109 text

Duplicating Your Production Code In Your Test Is A Test Smell Thursday, September 29, 11

Slide 110

Slide 110 text

This Means Don’t Mock Boundary Objects Thursday, September 29, 11

Slide 111

Slide 111 text

module Codebreaker describe Game do describe "#start" do it "sends a welcome message" do output = double('output') game = Game.new(output) output.should_receive(:puts) .with('Welcome to Codebreaker!') game.start end end end end Thursday, September 29, 11

Slide 112

Slide 112 text

module Codebreaker class Game def initialize(output) @output = output end def start @output.puts(“Welcome to Codebreaker”) end end end Thursday, September 29, 11

Slide 113

Slide 113 text

What Should We Do? Thursday, September 29, 11

Slide 114

Slide 114 text

Mock The Role In The Domain Object Thursday, September 29, 11

Slide 115

Slide 115 text

module Codebreaker describe Game do describe "#start" do it "displays a welcome message" do displayer = double('displayer') game = Game.new(displayer) displayer.should_receive(:display) .with('Welcome to Codebreaker!') game.start end end end end Thursday, September 29, 11

Slide 116

Slide 116 text

module Codebreaker class Game def initialize(displayer) @displayer = displayer end def start @displayer.display(“Welcome to Codebreaker”) end end end Thursday, September 29, 11

Slide 117

Slide 117 text

Implement The Display Object Using Puts Thursday, September 29, 11

Slide 118

Slide 118 text

How Is This Better? The point of the object is not to output to the command line, but to display the welcome. That could be to a GUI or a CLI, or something else. That is not the object’s responsibility to know. Thursday, September 29, 11

Slide 119

Slide 119 text

What About Testing The Display Object? Thursday, September 29, 11

Slide 120

Slide 120 text

Integration Tests / Acceptance Tests Thursday, September 29, 11

Slide 121

Slide 121 text

Only Mock Peers, Not Internals Thursday, September 29, 11

Slide 122

Slide 122 text

http://groups.google.com/group/growing-object-oriented- software/browse_thread/thread/ 5e84aa01d5e8854/8599991b3b1a390b?lnk=gst&q=internal +peer#8599991b3b1a390b “Growing Object Oriented Software Guided By Tests” Book Thursday, September 29, 11

Slide 123

Slide 123 text

Decide What Is Inside, And What Is Outside Your Object This is part of the DESIGN process! You have to decide where the border of your encapsulation is going to be. Thursday, September 29, 11

Slide 124

Slide 124 text

Not Everything Belongs To A Peer Thursday, September 29, 11

Slide 125

Slide 125 text

Key Question: Thursday, September 29, 11

Slide 126

Slide 126 text

Is This My Role? Thursday, September 29, 11

Slide 127

Slide 127 text

Example: Thursday, September 29, 11

Slide 128

Slide 128 text

Auction Server Auction Message Translator Process Message Thursday, September 29, 11

Slide 129

Slide 129 text

Different Types Of Messages Come In Thursday, September 29, 11

Slide 130

Slide 130 text

Translator Translates Them Into Domain Thursday, September 29, 11

Slide 131

Slide 131 text

describe AuctionMessageTranslator do it “notifies bid details when current price message received” do listener = double(‘event_listener’) listener.should_receive(:current_price) .with(192, 7) translator = AuctionMessageTranslator.new(listener) message = Message.new message.set_body(“SOLVersion: 1.1; Event: Price; CurrentPrice: 192; Increment: 7; Bidder: Someone else”) translator.process_message(message) end end Thursday, September 29, 11

Slide 132

Slide 132 text

class AuctionMessageTranslator def initialize(listener) @listener = listener end def process_message(message) event = unpack_event_from(message) if event.type == “CLOSE” @listener.auction_closed else @listener.current_price(event.current_price, event.increment) end end private: def unpack_event(message) AuctionMessageEvent.new(message) end end Thursday, September 29, 11

Slide 133

Slide 133 text

class AuctionMessageTranslator def initialize(listener) @listener = listener end def process_message(message) event = unpack_event_from(message) if event.type == “CLOSE” @listener.auction_closed else @listener.current_price(event.current_price, event.increment) end end private: def unpack_event(message) AuctionMessageEvent.new(message) end end Thursday, September 29, 11

Slide 134

Slide 134 text

class AuctionMessageTranslator def initialize(listener) @listener = listener end def process_message(message) event = unpack_event_from(message) if event.type == “CLOSE” @listener.auction_closed else @listener.current_price(event.current_price, event.increment) end end private: def unpack_event(message) AuctionMessageEvent.new(message) end end Thursday, September 29, 11

Slide 135

Slide 135 text

class AuctionMessageTranslator def initialize(listener) @listener = listener end def process_message(message) event = unpack_event_from(message) if event.type == “CLOSE” @listener.auction_closed else @listener.current_price(event.current_price, event.increment) end end private: def unpack_event(message) AuctionMessageEvent.new(message) end end Thursday, September 29, 11

Slide 136

Slide 136 text

class AuctionMessageTranslator def initialize(listener) @listener = listener end def process_message(message) event = unpack_event_from(message) if event.type == “CLOSE” @listener.auction_closed else @listener.current_price(event.current_price, event.increment) end end private: def unpack_event(message) AuctionMessageEvent.new(message) end end Thursday, September 29, 11

Slide 137

Slide 137 text

Internal Object Thursday, September 29, 11

Slide 138

Slide 138 text

Don’t Mock It Thursday, September 29, 11

Slide 139

Slide 139 text

Don’t Stub It Thursday, September 29, 11

Slide 140

Slide 140 text

Why? Thursday, September 29, 11

Slide 141

Slide 141 text

It’s An Implementation Detail Thursday, September 29, 11

Slide 142

Slide 142 text

Further Resources Thursday, September 29, 11

Slide 143

Slide 143 text

Thursday, September 29, 11

Slide 144

Slide 144 text

@gregmoeck [email protected] Questions? Thursday, September 29, 11