Mock Object Injection
Posted by James Mead Thu, 29 Nov 2007 12:17:48 GMT
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_somethingParameter 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_somethingWriter 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_somethingStubbed 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_somethingAny 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_somethingInstance 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
One issue with constructor injection is that it doesn’t work with Rails models. Obviously whether that’s an issue or not depends on whether you’re using Rails!
An issue with param injection is that you’ll be forced to pass around a collaborator with each method call. Could get ugly, so you move to constructor / writer method injection, with the issues they entail.
A problem that I think constructor and parameter injection both share is that the client code now has the dependency. It’s not a huge deal of course because dependency injection is a very common technique…but it is more indirection, makes client code slightly more complex, all for the sake of flexibility that we may not need. You can get around that a bit by setting default params, but that gets messy as soon as you have
I find Stubbed New Method to be very useful, mainly because we often don’t need the flexibility afforded by DI. When we do need more flexibility, it’s easy enough to refactor. There are problems that arise though because of the hidden dependency, as you mentioned. Sometimes tests will give you strange errors because you have no idea what’s going on under the hood.
Stubbed Private Method injection can be useful when trying to get legacy code under test. I would prefer to refactor towards a more typical form of DI once I’m confident in the test coverage.
I think it’s a good idea to design for testability, but I think Ruby is also powerful enough that we can break some rules in order to exercise more important design techniques. For example, both Stubbed New Method and Instance Variable Set Injection dig into the object and break encapsulation – but only from the test’s point of view. The object itself is still encapsulated, and you don’t write setter methods that are there only to facilitate testing.