An Introduction to Mock Objects in Ruby
James Mead
James Mead
Years later, Mock objects are still quite controversial, often misused and sometimes misunderstood.
“Pintside Thoughts: Mock Objects History”
—Tim McKinnon
nil
, Object.new
class Clock def time Time.now end end class Event def initialize(start_time) @start_time = start_time end def started?(clock = Clock.new) clock.time > @start_time end end
class StubClock def time Time.parse('2007-07-09 19:00') end end clock = StubClock.new meeting = Event.new(Time.parse('2007-07-09 18:00')) pub = Event.new(Time.parse('2007-07-09 20:00')) assert_equal true, meeting.started?(clock) assert_equal false, pub.started?(clock) # :-(
class Bell def start_ringing # ring, ring end end class Alarm def initialize(bell = Bell.new) @bell = bell end def trigger @bell.start_ringing end end
class BellSpy attr_reader :number_of_calls def initialize @number_of_calls = 0 end def start_ringing @number_of_calls += 1 end end bell = BellSpy.new alarm = Alarm.new(bell) alarm.trigger assert_equal 1, bell.number_of_calls
class Track def move(steps) # drive motor end end class Tank def initialize(left = Track.new, right = Track.new) @left, @right = left, right end def turn_right @left.move(+5) @right.move(-5) end end
class MockTrack include Test::Unit::Assertions def initialize(expected_steps) @expected_steps = expected_steps @actual_steps = 0 end def move(steps) @actual_steps += steps end def verify assert_equal @expected_steps, @actual_steps end end
left_track = MockTrack.new(+5) right_track = MockTrack.new(-5) tank = Tank.new(left_track, right_track) tank.turn_right left_track.verify right_track.verify
A Fake Object is can be used as if it were the real thing. It usually has better performance than the real thing by simplifying the implementation e.g. in-memory database is not actually persistent if its process is restarted, but it is persistent for the duration of the tests. A Fake Object also usually avoids unhelpful side effects e.g. actually emailing your customers to tell them you’ve billed them successfully.
Rails “mocks” (i.e. where classes are re-opened in files in the test/mocks directory) often fall into this category.
left_track = Mock.new('left_track') right_track = Mock.new('right_track') tank = Tank.new(left_track, right_track) # configure mock objects left_track.expects(:move).with(+5) right_track.expects(:move).with(-5) tank.turn_right left_track.verify right_track.verify
left_track = mock('left_track') right_track = mock('right_track') tank = Tank.new(left_track, right_track) # configure mock objects left_track.move(+5) right_track.move(-5) # change to playback mode left_track.replay right_track.replay tank.turn_right left_track.verify right_track.verify
The configuration stage looks quite nice, because you actually call methods on the mock in record mode to indicate what you are expecting. However, I think you lose an element of double-entry book-keeping – in that the test can look almost exactly the same as the implementation. Also it’s not always quite as elegant as it looks in this example, because you might also need to specify return values – EasyMock does this using a setReturnValue method.
This is a question that gets asked a lot. Mock Objects are most useful in testing a single class in isolation, helping you focus on testing the behaviour of just that one class. That one class is the real implementation not a mock – so not everything is mocked.
However, it’s right to be concerned that these unit tests do not guarantee that the system will behave how you expect. For that you need integration/acceptance tests which don’t use Mock Objects and which check that the real objects collaborate correctly.
I’d suggest it’s best to aim for high test coverage in your unit tests, but focus test coverage in your integration/acceptance tests on critical business scenarios. This is because unit tests run fast and failures in unit tests are more easily diagnosed.
left_track = mock('left_track') right_track = mock('right_track') tank = Tank.new(left_track, right_track) left_track.expects(:move).with(+5) right_track.expects(:move).with(-5) tank.turn_right # behaviour verification left_track.verify right_track.verify
tank = Tank.new initial_left_track_position = tank.left_track.position initial_right_track_position = tank.right_track.position tank.turn_right # state verification assert_equal +5, tank.left_track.position - initial_left_track_position assert_equal -5, tank.right_track.position - initial_right_track_position
Using Mock Objects in unit tests allows us to completely isolate object under test. A unit test for a specific class should be decoupled from the implementation of any of that class’s collaborators.
The Law of Demeter which basically says “only talk to your immediate friends” suggests that its a bad idea to unnecessarily expose object state.
Doing test-driven development using Mock Objects, you can drive out the public interface you need for an object before you’ve implemented it.
# expect instance method duck = mock('duck') duck.expects(:quack) duck.quack # => verify success
# expect multiple invocations duck = mock('duck') duck.expects(:quack).times(3) duck.quack duck.quack duck.quack # => verify success
# specifying parameters duck = mock('duck') duck.expects(:waddle).with(2.metres) duck.waddle(1.metre) # => verify failure
# specifying some parameters duck = mock('duck') duck.expects(:swim).with(anything, 1.metre_per_second) duck.swim(2.metres, 1.metre_per_second) # => verify success
# return values and exceptions duck = mock('duck') duck.stubs(:flap_wings).returns(true,true,true).then.raises(BrokenWingError) duck.flap_wings # => true duck.flap_wings # => true duck.flap_wings # => true duck.flap_wings # => raises BrokenWingError
# shortcuts duck = stub(:animal? => true, :bird? => true) duck.animal? # => true duck.bird? # => true duck = stub_everything(:bird? => true) duck.animal? # => nil duck.mineral? # => nil duck.bird? # => true
# stubbing instance method donald = Duck.new donald.stubs(:flap_wings).returns(true) daffy = Duck.new daffy.stubs(:flap_wings).raises(BrokenWingError) donald.flap_wings # => true daffy.flap_wings # => raises BrokenWingError
# stubbing class method Duck.stubs(:animal?).returns(false) # stubbed implementation Duck.animal? # => false # original implementation unchanged Duck.animal? # => true
# stubbing instance method on any instance of a class daisy = Duck.new daisy.bird? # => true # original implementation Duck.any_instance.stubs(:bird?).returns(false) daffy = Duck.new daffy.bird? # => false donald = Duck.new donald.bird? # => false
attr_writer
new
method on classany_instance
instance_variable_set
class ClassUnderTest def initialize(dependency = Collaborator.new) @dependency = dependency end def do_something # use @dependency end end collaborator = mock('collaborator') # constructor parameter injection instance_under_test = ClassUnderTest.new(collaborator) instance_under_test.do_something
class ClassUnderTest def do_something(local_dependency = Collaborator.new) # use local_dependency end end collaborator = mock('collaborator') instance_under_test = ClassUnderTest.new # method parameter injection instance_under_test.do_something(collaborator)
class ClassUnderTest attr_writer :dependency def initialize @dependency = Collaborator.new end def do_something # use @dependency end end collaborator = mock('collaborator') instance_under_test = ClassUnderTest.new # writer method injection instance_under_test.dependency = collaborator instance_under_test.do_something
class ClassUnderTest def initialize @dependency = Collaborator.new end def do_something # use @dependency end end collaborator = mock('collaborator') # stubbed new method injection Collaborator.stubs(:new).returns(collaborator) instance_under_test = ClassUnderTest.new instance_under_test.do_something
class ClassUnderTest def do_something local_dependency = build_collaborator() # use local_dependency end private def build_collaborator Collaborator.new end end collaborator = mock('collaborator') instance_under_test = ClassUnderTest.new # stubbed private method injection instance_under_test.stubs(:build_collaborator).returns(collaborator) instance_under_test.do_something
class ClassUnderTest def do_something local_dependency = Collaborator.new return local_dependency.do_stuff end end # any_instance stub injection Collaborator.any_instance.stubs(:do_stuff).return('something useful') instance_under_test = ClassUnderTest.new instance_under_test.do_something
class ClassUnderTest def initialize @dependency = Collaborator.new end def do_something # use @dependency end end collaborator = mock('collaborator') instance_under_test = ClassUnderTest.new # instance_variable_set injection instance_under_test.instance_variable_set(:@dependency, collaborator) instance_under_test.do_something
Source: C2 Wiki – Law of Demeter