Mock Object Injection

A few months back, in my Introduction to Mock Objects talk at LRUG, I talked about “Mock Object Injection”. At the time I described a number of different ways of replacing a production object with a Mock Object using Mocha. I remember that at the meeting, James Adam (who has since joined the team at Reevoo) asked me why I didn’t like the Any Instance Stub Injection technique.

I’m not sure I gave him a very convincing response and I’ve been meaning for ages to have a better go at explaining what I think are the pros and cons of each of the techniques I mentioned. Here’s the list of techniques with the ones I like best at the top. I still haven’t done a very good job, but I’d be interested to hear what other people think so that I can try and improve my understanding.

Constructor Injection

The ClassUnderTest allows its dependencies to be passed in as parameters to its constructor. A mock object is passed in as a replacement for the “real” collaborator. It may be convenient to specify the production collaborator as a default parameter value.

Advantages

The dependencies of the ClassUnderTest are explicit.

Disadvantages

Can’t think of any at the moment.

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

Parameter Injection

The ClassUnderTest allows its dependencies to be passed in as parameters to the method under test. A mock object is passed in as a replacement for the “real” collaborator. It may be convenient to specify the production collaborator as a default parameter value.

Advantages

The dependencies of the method under test are explicit.

Disadvantages

Can’t think of any at the moment.

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)

Stubbed New Method Injection

Use Mocha’s Object#stubs to temporarily replace Collaborator#new with a stub implementation that returns a mock object.

Advantages

Better than Any Instance Stub Injection, because you can have more control over different instances of Collaborator.

Disadvantages

Dependencies of the ClassUnderTest are hidden and not explicit.

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

Writer Method Injection

Use an attribute writer method to replace the “real” collaborator with a mock object.

Disadvantages

The ClassUnderTest has to unnecessarily expose a way to modify its internal state. The test is coupled to the implementation of the ClassUnderTest.

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

Stubbed Private Method Injection

Use partial mocking to temporarily replace a private builder method with a stubbed version of the method.

Disadvantages

The test is coupled to the implementation of the ClassUnderTest. The partial mocking of the instance_under_test means that the test is not testing a pristine instance of the ClassUnderTest, but a modified one. It also means that the boundaries between test code and production code are less clear.

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

Any Instance Stub Injection

Use Mocha’s Class#any_instance method to temporarily replace the method on a collaborator with a stub method.

Disadvantages

The stubbed method is applied to all instances of the collaborating class. If the instance_under_test interacts with the stubbed method on more than one instance of the collaborating class, it isn’t possible to specify different behaviour for the stubbed method on each instance. Even if the instance_under_test only interacts with the stubbed method on one instance of the collaborating class, the test is specifying more stubbed behaviour than strictly necessary which could lead to false positives.

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

Instance Variable Set Injection

Use Object#instance_variable_set to replace the reference to a collaborator with a mock object.

Disadvantages

The test is coupled to the implementation of the ClassUnderTest. In particular the test is coupled to the supposedly private instance variable. In my opinion, it would be more honest to expose the instance variable by adding an attribute writer and using Writer Method Injection.

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