Mocks Enabling
Test-Driven Design
Alexandre Martins
http://m.artins.net
Twitter: @amartinsn
Mocking Hell
http://blog.m.artins.net/tdd-listen-to-the-tests-
they-tell-smells-in-your-code/
Listen ToYour Tests,
They TellYou Smells
...
Duplicated
Implementation
describe ShowController, “index” do
context “when a TV show has no public videos” do
it “should not display any shows” do
...
class ShowController
def index
@shows = Show.all(
:select => “id, name, video_count”,
:conditions => “shows.video_count > ...
describe ShowController, “index” do
context “when a TV show has no public videos” do
it “should not display any shows” do
...
class ShowController
def index
@shows = Show.all(
:select => “id, name, video_count”,
:conditions => “shows.video_count > ...
Leads To Brittle
Tests That Do
A Poor Job
module Codebreaker
describe Game do
describe “#start” do
it “sends a welcome message” do
output = double(‘output’)
game = ...
module Codebreaker
class Game
def initialize(output)
@output = output
end
def start
@output.puts(“Welcome to
Codebreaker!”...
Refactor To The
Following:
module Codebreaker
class Game
def initialize(output)
@output = output
end
def start
@output.write(“Welcome to
Codebreaker!...
The Tests Fail!
The Output Does
Not Change
Mocks Suck?
Maybe
Or Maybe We Just
Don’t Understand
Them
Why Mocks?
MockObjects
+
Procedural
Programming
=
Bad Idea!
Mocks Are
Not Stubs!
Mocks
Assert On
Messages
Stubs Return
Values
describe ShowController, “index” do
context “when a TV show has no public videos” do
it “should not display any shows” do
...
MockObjects
+
OOP
=
Good Idea!
Key Idea on
OOP:
Objects Tell
Objects To
Do Things
“The key in making great and growable
systems is much more to design how
its modules communicate rather than
what their in...
Change The
Behavior Of
The System By
Composing
Objects
Example:
Ticket Machine
Interface
Ticket Reserve
System
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
ImagineYou Want
To Audit The
Requests Before
Processing Them...
Procedural
Programming
Way:Add Code
To The Method
For OO
Programming This
Violates The Single
Responsibility
Principle
Object-Oriented
Programming Way:
Change Object
Composition
Ticket Machine
Interface
Ticket Request
Auditing
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Ticket Reser...
What Would
The Code For
The Interface
Look Like?
class TicketMachineInterface
def initialize(request_handler)
@request_handler = request_handler
@current_display = “”
end
...
Notice Two
Things:
1. Follows Tell,
Don’t Ask
Principle
class TicketMachineInterface
def initialize(request_handler)
@request_handler = request_handler
@current_display = “”
end
...
2. Hides Its
Internal State
class TicketMachineInterface
def initialize(request_handler)
@request_handler = request_handler
@current_display = “”
end
...
How Can We
Assert On
State?
You Would
Have To Add
Getters Just
For Tests?
Bad Idea!
How ThenYou
Do Tests?
Ticket Machine
Interface
Ticket Reserve
System
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Ticket Machine
Interface
Ticket Reserve
System
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Assert On The ...
How?
Ticket Machine
Interface
Fake Object
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Ticket Machine
Interface
Mock Object
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
describe TicketMachineInterface do
it “reserves the number of tickets inputted when
user submits the request” do
request_h...
describe TicketMachineInterface do
it “reserves the number of tickets inputted when
user submits the request” do
request_h...
describe TicketMachineInterface do
it “reserves the number of tickets inputted when
user submits the request” do
request_h...
Ticket Machine
Interface
Mock Object
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
describe TicketMachineInterface do
it “reserves the number of tickets inputted when
user submits the request” do
request_h...
Key Idea
In OOP,
Behavior Is
Found In The
Messages
Mocks Assert
On The
Messages Going
Between The
Objects
Key
Mocking
Rules...
1. Mock Roles,
Not Objects
http://www.jmock.org/oopsla2004.pdf
Wanting To Mock
Concrete Objects
Is A Design Smell
Well Design
Object Don’t
Know Explicitly
Who They Are
Talking To
Why?
Because Who
They’re Talking To
Can Change
They Should Only
Know The ROLE
Their Collaborator
Is Playing
Ticket Machine
Interface
Ticket Reserve
System
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Ticket Machine...
Ticket Machine
Interface
Ticket Request
Auditing
Reserve Tickets
Number Pressed
Delete Pressed
Submit Request
Ticket Reser...
When Mocking
Roles,TDD
Becomes A
Design Process
class TicketMachineInterface
def number_pressed(number)
end
def delete_pressed
end
def submit_request
end
end
describe TicketMachineInterface do
it “reserves the correct number of tickets when a
number is pressed two times before su...
describe TicketMachineInterface do
it “reserves the number of tickets inputted when
user submits the request” do
request_h...
class TicketMachineInterface
def initialize(request_handler)
@request_handler = request_handler
end
def number_pressed(num...
class TicketMachineInterface
def initialize(request_handler)
@request_handler = request_handler
@current_display = “”
end
...
2. Only Mock
Types That
You Own
IfYou Don’t
Own The API,
There’s No
Design Feedback
Instead Of
Mocking A Role,
You’re
Mocking An
Implementation
DuplicatingYour
Production Code
InYour Test Is A
Test Smell
This Means
Don’t Mock
Boundary
Objects
module Codebreaker
describe Game do
describe “#start” do
it “sends a welcome message” do
output = double(‘output’)
game = ...
module Codebreaker
class Game
def initialize(output)
@output = output
end
def start
@output.puts(“Welcome to
Codebreaker!”...
What Should
We Do?
Mock The Role
In The Domain
Object
module Codebreaker
describe Game do
describe “#start” do
it “sends a welcome message” do
displayer = double(‘displayer’)
g...
module Codebreaker
class Game
def initialize(displayer)
@displayer = displayer
end
def start
@displayer.display(“Welcome
t...
Implement The
Displayer Role
Using Puts
How Is This Any
Better?
Now The
Responsibility of The
Game Is To Welcome
New Players Through
A Displayer
Now I Can Move From
Displaying The Message
On A Command-Line
Interface To A
RichGUI...
...Without Having To
Change Codebreaker
Object!
Open/Closed Principle
“Where Objects
Should Be Open For
Extension, But Closed
For Modification”
-Bob Martin
What About
Testing The
Display Object?
Integration Tests
/ Acceptance
Tests
3. Only Mock
Peers Not
Internals
Decide What
Behavior Is Inside
And What’s
OutsideYour
Object
Does this implementation belong internal to my
object, or as a collaborator to my object?
Single Responsibility
Principle
“A Class Should Have
Only One Reason To
Change”
-Bob Martin
Is This My Role?
Auction Server
Auction Message
Translator
Process
Message
Different Types
Of Messages
Come In
Translator
Translates Them
Into Domain
describe AuctionMessageTranslator
it “notifies bid details when current price message
received” do
listener = double(‘even...
class AuctionMessageTranslator
def initialize(listener)
@listener = listener
end
def process_message(message)
event = unpa...
class AuctionMessageTranslator
def initialize(listener)
@listener = listener
end
def process_message(message)
event = unpa...
class AuctionMessageTranslator
def initialize(listener)
@listener = listener
end
def process_message(message)
event = unpa...
Internal
Object
Don’t Mock It!
Don’t Stub It!
Why?
It’s An
Implementation
Detail
Wanna Learn
More?
Read:
Thanks!
Próximos SlideShares
Carregando em…5
×

Mocks Enabling Test-Driven Design

68 visualizações

Publicada em

How to make use of mocks to enable better software design through tests.

Publicada em: Software
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

Mocks Enabling Test-Driven Design

  1. 1. Mocks Enabling Test-Driven Design Alexandre Martins http://m.artins.net Twitter: @amartinsn
  2. 2. Mocking Hell
  3. 3. http://blog.m.artins.net/tdd-listen-to-the-tests- they-tell-smells-in-your-code/ Listen ToYour Tests, They TellYou Smells InYour Code
  4. 4. Duplicated Implementation
  5. 5. 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
  6. 6. class ShowController def index @shows = Show.all( :select => “id, name, video_count”, :conditions => “shows.video_count > 0” ) end end
  7. 7. 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
  8. 8. class ShowController def index @shows = Show.all( :select => “id, name, video_count”, :conditions => “shows.video_count > 0” ) end end
  9. 9. Leads To Brittle Tests That Do A Poor Job
  10. 10. 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
  11. 11. module Codebreaker class Game def initialize(output) @output = output end def start @output.puts(“Welcome to Codebreaker!”) end end end
  12. 12. Refactor To The Following:
  13. 13. module Codebreaker class Game def initialize(output) @output = output end def start @output.write(“Welcome to Codebreaker!”) end end end
  14. 14. The Tests Fail!
  15. 15. The Output Does Not Change
  16. 16. Mocks Suck?
  17. 17. Maybe
  18. 18. Or Maybe We Just Don’t Understand Them
  19. 19. Why Mocks?
  20. 20. MockObjects + Procedural Programming = Bad Idea!
  21. 21. Mocks Are Not Stubs!
  22. 22. Mocks Assert On Messages
  23. 23. Stubs Return Values
  24. 24. 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 Asserting on State!
  25. 25. MockObjects + OOP = Good Idea!
  26. 26. Key Idea on OOP:
  27. 27. Objects Tell Objects To Do Things
  28. 28. “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 Inventor of Smalltalk
  29. 29. Change The Behavior Of The System By Composing Objects
  30. 30. Example:
  31. 31. Ticket Machine Interface Ticket Reserve System Reserve Tickets Number Pressed Delete Pressed Submit Request
  32. 32. ImagineYou Want To Audit The Requests Before Processing Them...
  33. 33. Procedural Programming Way:Add Code To The Method
  34. 34. For OO Programming This Violates The Single Responsibility Principle
  35. 35. Object-Oriented Programming Way: Change Object Composition
  36. 36. Ticket Machine Interface Ticket Request Auditing Reserve Tickets Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets
  37. 37. What Would The Code For The Interface Look Like?
  38. 38. 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
  39. 39. Notice Two Things:
  40. 40. 1. Follows Tell, Don’t Ask Principle
  41. 41. 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
  42. 42. 2. Hides Its Internal State
  43. 43. 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
  44. 44. How Can We Assert On State?
  45. 45. You Would Have To Add Getters Just For Tests?
  46. 46. Bad Idea!
  47. 47. How ThenYou Do Tests?
  48. 48. Ticket Machine Interface Ticket Reserve System Reserve Tickets Number Pressed Delete Pressed Submit Request
  49. 49. Ticket Machine Interface Ticket Reserve System Reserve Tickets Number Pressed Delete Pressed Submit Request Assert On The Message
  50. 50. How?
  51. 51. Ticket Machine Interface Fake Object Reserve Tickets Number Pressed Delete Pressed Submit Request
  52. 52. Ticket Machine Interface Mock Object Reserve Tickets Number Pressed Delete Pressed Submit Request
  53. 53. describe TicketMachineInterface do it “reserves the number of tickets inputted when user submits the 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
  54. 54. describe TicketMachineInterface do it “reserves the number of tickets inputted when user submits the 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
  55. 55. describe TicketMachineInterface do it “reserves the number of tickets inputted when user submits the 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
  56. 56. Ticket Machine Interface Mock Object Reserve Tickets Number Pressed Delete Pressed Submit Request
  57. 57. describe TicketMachineInterface do it “reserves the number of tickets inputted when user submits the 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
  58. 58. Key Idea
  59. 59. In OOP, Behavior Is Found In The Messages
  60. 60. Mocks Assert On The Messages Going Between The Objects
  61. 61. Key Mocking Rules...
  62. 62. 1. Mock Roles, Not Objects
  63. 63. http://www.jmock.org/oopsla2004.pdf
  64. 64. Wanting To Mock Concrete Objects Is A Design Smell
  65. 65. Well Design Object Don’t Know Explicitly Who They Are Talking To
  66. 66. Why?
  67. 67. Because Who They’re Talking To Can Change
  68. 68. They Should Only Know The ROLE Their Collaborator Is Playing
  69. 69. Ticket Machine Interface Ticket Reserve System Reserve Tickets Number Pressed Delete Pressed Submit Request Ticket Machine Request Handler (ROLE)
  70. 70. Ticket Machine Interface Ticket Request Auditing Reserve Tickets Number Pressed Delete Pressed Submit Request Ticket Reserve System Reserve Tickets Ticket Machine Request Handler (ROLE)
  71. 71. When Mocking Roles,TDD Becomes A Design Process
  72. 72. class TicketMachineInterface def number_pressed(number) end def delete_pressed end def submit_request end end
  73. 73. 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
  74. 74. describe TicketMachineInterface do it “reserves the number of tickets inputted when user submits the 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 Not the actual object being implemented, but the role that object is going to play
  75. 75. 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
  76. 76. 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
  77. 77. 2. Only Mock Types That You Own
  78. 78. IfYou Don’t Own The API, There’s No Design Feedback
  79. 79. Instead Of Mocking A Role, You’re Mocking An Implementation
  80. 80. DuplicatingYour Production Code InYour Test Is A Test Smell
  81. 81. This Means Don’t Mock Boundary Objects
  82. 82. 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
  83. 83. module Codebreaker class Game def initialize(output) @output = output end def start @output.puts(“Welcome to Codebreaker!”) end end end We’re coupling it to the actual Ruby standard output The responsibility of this game is to talk to a command-line interface
  84. 84. What Should We Do?
  85. 85. Mock The Role In The Domain Object
  86. 86. module Codebreaker describe Game do describe “#start” do it “sends 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
  87. 87. module Codebreaker class Game def initialize(displayer) @displayer = displayer end def start @displayer.display(“Welcome to Codebreaker!”) end end end
  88. 88. Implement The Displayer Role Using Puts
  89. 89. How Is This Any Better?
  90. 90. Now The Responsibility of The Game Is To Welcome New Players Through A Displayer
  91. 91. Now I Can Move From Displaying The Message On A Command-Line Interface To A RichGUI...
  92. 92. ...Without Having To Change Codebreaker Object!
  93. 93. Open/Closed Principle “Where Objects Should Be Open For Extension, But Closed For Modification” -Bob Martin
  94. 94. What About Testing The Display Object?
  95. 95. Integration Tests / Acceptance Tests
  96. 96. 3. Only Mock Peers Not Internals
  97. 97. Decide What Behavior Is Inside And What’s OutsideYour Object
  98. 98. Does this implementation belong internal to my object, or as a collaborator to my object?
  99. 99. Single Responsibility Principle “A Class Should Have Only One Reason To Change” -Bob Martin
  100. 100. Is This My Role?
  101. 101. Auction Server Auction Message Translator Process Message
  102. 102. Different Types Of Messages Come In
  103. 103. Translator Translates Them Into Domain
  104. 104. describe AuctionMessageTranslator 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(“SOLVersion: 1.1; Event: Price; CurrentPrice: 192; Increment: 7; Bidder: Someone Else”) translator.process_message(message) end end
  105. 105. 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_from(message) AuctionMessageEvent.new(message) end end
  106. 106. 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_from(message) AuctionMessageEvent.new(message) end end
  107. 107. 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_from(message) AuctionMessageEvent.new(message) end end
  108. 108. Internal Object
  109. 109. Don’t Mock It!
  110. 110. Don’t Stub It!
  111. 111. Why?
  112. 112. It’s An Implementation Detail
  113. 113. Wanna Learn More?
  114. 114. Read:
  115. 115. Thanks!

×