Practical PHP Testing Patterns: Dependency Injection
Join the DZone community and get the full member experience.
Join For FreeUnit testing, in fact, requires to isolate an object, or a small group of objects. These Systems Under Test have to provide a seam where we can substitute the real object, used in production, with a Test Double. The simplest, and simultaneously effective, technique for providing a seam is Dependency Injection: make objects ask for dependencies instead of going around to grab them from static sources or global variables.
Implementation
Objects declare a dependency by requiring the corresponding collaborator via a constructor parameter, a setter, or an interface method. The constructor approach is going to cut it in most of greenfield cases, while for refactoring from existing code setters are less invasive..
In the test, we pass in a Test Double instead a collaborator during the arrange phase. This Test Double is an alternate implementation created via subclassing or via an implementation of the required interface (to satisfy type hints, the only compile-time checks of PHP applications). The test focus on the object at hand, since the injected Test Doubles simplify a lot the picture.
In production of course, the real collaborator is injected, in order to form a whole object graph.
Dependency Injection usually simplifies very much the setup phase of the tests, but also improves the production code by cutting assumptions about dependency wiring; the actual objects satisfying a dependency are chosen during the application bootstrap, and the related decisions are not spread in hundreds of constructors.
Although an object featuring Dependency Injection will be more complex to construct, one-time construction responsibility will be kept separated from the responsibilities of the object. This leads to light __construct() methods, which only assign parameters to fields, and to the use of Factories or Dependency Injection containers.
By no means you need an external library like a DI container to implement this pattern: you can perfectly write Factory classes on your own.
The joy of refactoring
If you do not have a seam, when you want to inject a dependency you should definitely introduce an interface and an injection point like a constructor parameter. You may have to revise the contract with the collaborator: it may be too chatty to use a Test Double, calling 6 methods with disparate parameters.Variations
- Constructor Injection enforces one-time initialization, and the injected collaborator can never be altered nor the rest of the Api is affected by having to contain new methods for injection. It's the perfect case.
- Setter Injection is reserved for classes that have many collaborators, a scenario which could also be a smell. Setters are also easy to treat for automated dependency injection, where you do not construct the production version of the object by hand in a factory, but use DI frameworks or some reflection (method_exists()) to save work. For example, you can have a bunch of standard setter that when seen on a certain category of objects cause the injection of standard collaborators.
- In his xUnit Testing Patterns book, Meszaros proposes a third variation for injection over the classical constructor and setter: Method Parameter Injection. In fact, injection via method parameter is common when the other methods do not require the same collaborator; in those cases, it's not even recognized as Dependency Injection by many TDDers, but just as good object-oriented programming (the only alternative using a singleton.)
- Fowler instead proposes Interface Injection as the third variation, where the list of setters is formalized in an interface. I don't feel like we strictly need this solution very much in a dynamic language like PHP, although if the interface may be extracted in case it presents itself as a meaningful concept (no DependentOnEntityManagerAndByTheWayAllDoctrine2Interface).
Example
In the code sample injecting a collaborator is compared with inline new() in case the correspondent production class does not exist yet. Testing classes implementing Dependency Injection is closely related to Test Double patterns: Dependency Injection paves the way for them.
<?php class DependencyInjectionTest extends PHPUnit_Framework_TestCase { /** * The System Under Test is exercised in isolation via a . */ public function testInjectsATestDoubleViaConstructor() { $collaboratorStub = $this->getMock('Collaborator'); $sut = new GoodSut($collaboratorStub); $collaboratorStub->expects($this->any())->method('baseValue')->will($this->returnValue(5)); $this->assertEquals(50, $sut->calculateValue()); } /** * The System Under Test cannot be exercised in isolation, and since * the other class is not yet finished we're blocked. We cannot even * substitute Collaborator just because of speed if it's a really heavy * and complex to set up implementation using a database or the filesystem. */ public function testCannotSubstituteTheCollaborator() { $sut = new BadSut(); // now what? $this->markTestIncomplete('Kernel panic!'); } } class GoodSut { private $collaborator; public function __construct(Collaborator $collaborator) { $this->collaborator = $collaborator; } public function calculateValue() { return $this->collaborator->baseValue() * 10; } } class BadSut { private $collaborator; public function __construct() { $this->collaborator = new Collaborator(); } public function calculateValue() { return $this->collaborator->baseValue() * 10; } } /** * The Collaborator class is not even finished yet. */ class Collaborator { public function baseValue() {} }
Opinions expressed by DZone contributors are their own.
Comments