Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why You Don't Get Mock Objects

gmoeck
September 29, 2011

Why You Don't Get Mock Objects

Although the Ruby community has embraced TDD like no other community ever has, we have always looked at mock objects with disdain, and perhaps even a little hatred. I've heard conference speakers call them "Wastes of time", "Scams", and even "Testing Heresies". Why would anyone have ever developed these pieces of junk? In reality though many in the agile community have embraced mock objects within their testing cycles because they have found using these fake objects improves the design of their systems. But why not Rubyists? Most Rubyists don't get mock objects because they don't understand their history or their creators intent. They try to fit them into their current workflow without understanding them, and find them unhelpful and stupid. After all, almost all of the good literature on the subject is written in Java, and we know how frustrating it is to read Java when your used to Ruby. As such, this talk will attempt to demonstrate in Ruby the usefulness of mock objects, and how to use them to improve the design of your system. Specifically the following will be covered:
* Why do we need mock objects (Following the 'Tell, Don't Ask Principle')
* Why you should only mock objects you own
* Why you shouldn't use mocks to test boundary objects like external API calls
* Why you should mock roles, not Objects
* Why you should only mock your immediate neighbors
* Why listening to your unit tests will tell you about design problems in your code

gmoeck

September 29, 2011
Tweet

More Decks by gmoeck

Other Decks in Programming

Transcript

  1. “[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
  2. 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
  3. class ShowController def index @shows = Show.all( :select => “id,

    name, video_count”, :conditions =>“shows.video_count > 0” ) end end Thursday, September 29, 11
  4. 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
  5. class ShowController def index @shows = Show.all( :select => “id,

    name, video_count”, :conditions =>“shows.video_count > 0” ) end end Thursday, September 29, 11
  6. 2. Lead To Brittle Tests That Do A Poor Job

    Thursday, September 29, 11
  7. 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
  8. module Codebreaker class Game def initialize(output) @output = output end

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

    def start @output.writes(“Welcome to Codebreaker”) end end end Thursday, September 29, 11
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. “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
  16. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket

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

    Reserve System Reserve Tickets Persist Ticket Requests Reserve Tickets Thursday, September 29, 11
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket

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

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

    Reserve System Reserve Tickets Thursday, September 29, 11
  30. 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
  31. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock

    Object Reserve Tickets Thursday, September 29, 11
  32. 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
  33. 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
  34. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock

    Object Reserve Tickets Thursday, September 29, 11
  35. 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
  36. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Mock

    Object Reserve Tickets Thursday, September 29, 11
  37. 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
  38. 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
  39. 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
  40. Ticket Machine Interface Number Pressed Delete Pressed Submit Request Ticket

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

    Reserve System Reserve Tickets Ticket Machine Request Handler Thursday, September 29, 11
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. If You Don’t Own The API, There Is No Design

    Feedback Thursday, September 29, 11
  49. 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
  50. module Codebreaker class Game def initialize(output) @output = output end

    def start @output.puts(“Welcome to Codebreaker”) end end end Thursday, September 29, 11
  51. 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
  52. module Codebreaker class Game def initialize(displayer) @displayer = displayer end

    def start @displayer.display(“Welcome to Codebreaker”) end end end Thursday, September 29, 11
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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