An Introduction to Mock Objects in Ruby

James Mead

Outline

  • What is a Mock Object?
  • Why use Mock Objects?
  • Examples using Mocha
  • Hints and Tips

Outline

  • What is a Mock Object?
  • Why use Mock Objects?
  • Examples using Mocha
  • Hints and Tips

What is a Mock Object?

Years later, Mock objects are still quite controversial, often misused and sometimes misunderstood.

“Pintside Thoughts: Mock Objects History”

—Tim McKinnon

What is a Mock Object?

  • Different things to different people
  • Ambiguous terminology
  • Confusion with Rails “mocks”
  • Learn from the Java & TDD community

Test Double

  • xUnit Test Patterns (Gerard Meszaros)
  • Think “stunt double”
  • Replace production object for testing purposes

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Dummy Object

  • Trivial, placeholder
  • No behaviour relevant to test
  • e.g. nil, Object.new

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Test Stub

  • Canned return values
  • May only respond to calls needed for test
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) # :-(

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Test Spy

  • Records method invocations
  • Test makes assertions on Spy after the event
  • May incorporate behaviour of Test Stub
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

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Mock Object

  • Expected method invocations set in advance
  • Verifies actual invocations match expected ones
  • May incorporate behaviour of Test Stub
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

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Fake Object

  • Mostly working implementation
  • Lightweight, unsuitable for production
  • e.g. in-memory database

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.

Types of Test Doubles

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Hard-coded or Configurable

  • Dummy Object
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

Configurable Mock Object

  • Single generic Mock class
  • Not multiple concrete Mock classes
  • Test configures expected invocations
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

Record and Playback

  • Mock Object has two modes
  • Record – test calls expected methods
  • Playback – actual methods called
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.

Outline

  • What is a Mock Object?
  • Why use Mock Objects?
  • Examples using Mocha
  • Hints and Tips

If everything’s mocked, what’s actually being tested?

  • Mocks are most useful for unit testing
  • Integration tests demonstrate actual collaboration
  • Fake objects are more useful in integration tests

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.

Behaviour vs State Verification

  • Behaviour – test interactions between objects
  • State – test results of those interactions
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

Why use Mock Objects?

  • Less interaction with e.g database
  • No need to unnecessarily expose state
  • Less duplicate coverage
  • Faster tests
  • Reduced coupling
  • Enhanced TDD

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.

Ruby Mock Object Libraries

  • Mocha
  • RSpec Mocks
  • Flexmock

Mocha

  • Harvested from projects at Reevoo
  • Ben Griffiths, Chris Roos & Paul Battley
  • Simple, readable syntax inspired by JMock
  • Unified syntax for traditional & partial mocking
  • Pronounced Mokker not Moker

Outline

  • What is a Mock Object?
  • Why use Mock Objects?
  • Examples using Mocha
  • Hints and Tips

Mocha Examples

  • Traditional Mocking
  • Partial Mocking

Mocha Examples

  • Traditional Mocking
  • Partial Mocking
# 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

Mocha Examples

  • Traditional Mocking
  • Partial Mocking
# 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

Outline

  • What is a Mock Object?
  • Why use Mock Objects?
  • Examples using Mocha
  • Hints and Tips

Mock Injection

  • Method or constructor parameter (default value)
  • Writer method i.e. attr_writer
  • Stub new method on class
  • Stubbed method (private)
  • Stub methods using any_instance
  • Use 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

Command vs Query

  • Command method
    • changes an object’s state
    • returns at most indication of success/failure
  • Query method
    • does not change an object’s state
    • returns an aspect of object’s state
  • Expect commands, stub queries

Mocking External Services

  • Don’t attempt to mock directly
  • Wrap them with an application-specific adapter
  • Mock the adapter

Fragile Tests

  • Avoid one-for-one matching of expectation & code
  • Minimally constrain the code under test
  • Don’t expect when you could stub
  • Specify range instead of exact value

The End

  • You can play with yourself
  • You can play with your own toys
  • You can play with toys that were given to you
  • And you can play with toys you’ve made yourself

Source: C2 Wiki – Law of Demeter

References

  • Mock Objects (http://www.mockobjects.com)
  • xUnit Test Patterns (http://xunitpatterns.com)
  • JMock (http://www.jmock.org)
  • Martin Fowler (http://www.martinfowler.com)