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