Practical PHP Testing Patterns: Test Hook
Join the DZone community and get the full member experience.
Join For FreeThe Test Hook pattern is, again, an option for replacing dependencies at runtime. It is a very lightweight, and as so easy to use on legacy code: it does not require you to inject anything in the production code.
The pattern consists in modifying the SUT itself to remove the dependency, substitute it, or anyway simplyfing the test scenario. But the production code does not change its external behavior. This pattern is a measure of last resort: it allows you to deal with hardcoded class names, filesystem calls or to interrupt spooky actions-at-a-distance.
Implementation
This pattern is implemented by subclassing: one or more method overrides modify a part of the SUT. The targets may be a Factory Method or the hooks of Template Methods.
The production code can continue to use their own hardcoding for now, all we need is a little refactoring to encapsulate the creation of the collaborator in a single place that we can override. If we have to override a long method, we'll also have to rewrite part of it.
For substitution of Test Doubles, the target is strictly a Factory Method, but it's just a special case: the original implementation may not be related to creation, but just to grabbing the collaborator from some source (such as a Singleton or Registry). There are other options than substitute a Factory Method, similar to the Test-Specific Subclass approach: for example replacing a crucial method which calls a static class or a Singleton to simplify the test scenario.
A case for refactoring
Once you have got the tests running, why refactor away from a Test Hook? Indeed the introduction of Dependency Injection may take a while, as you have to look up all the places where the SUT is created and make sure the collaborators are passed to it. This kind of large-scale refactoring applies well to a whole application, when you start defining the lifecycle of your objects instead of letting them be defined locally by whoever calls new().
I think a good rule of thumb for extracting a collaborator instead of a Test Hook is seeing the possibility for substitution of the collaborator with another implementation, applying a naive form of the Strategy pattern: in our code sample, it would be displaying date and times extracted from some source instead of the current one. Replacing a collaborator is simpler than creating yet another subclass, and will pay you back for the time you take for the refactoring.
Example
In the code sample, a Test Hook is introduced into an HTML box displaying the current date. Since we do not want to adjust our assertion every day, the job of the hook is to provide a fixed date.<?php /** * The TestCase shows how first only a brittle test and a loose assertion * can be made over the result. With proper isolation, the assertion becomes * an equality: at the same time TestableTimeBox isn't going to break client * code where UntestableTimeBox was used, because we only changed an internal * detail and not the public Api (like its constructor). */ class TestHookTest extends PHPUnit_Framework_TestCase { public function testTheUntestableTimeBoxWithASmokeTest() { $box = new UntestableTimeBox(); $this->assertRegexp('/<div(.*)<\/div>/', $box->__toString()); } public function testTheTestableTimeBoxWithAUnitTest() { $box = new TestSubclassOfTestableTimeBox(0000001); $this->assertEquals('<div class="current_timestamp">1970-01-01</div>', $box->__toString()); } public function testTheTestableTimeBoxWithAUnitTestAndAPartialGeneratedMock() { $box = $this->getMock('TestableTimeBox', array('currentTime')); $box->expects($this->any()) ->method('currentTime') ->will($this->returnValue(0000001)); $this->assertEquals('<div class="current_timestamp">1970-01-01</div>', $box->__toString()); } } /** * The original SUT: there is no way to fully test it due to the global state * introduced by time(). */ class UntestableTimeBox { public function __toString() { return '<div class="current_timestamp">' . date('Y-m-d', time()) . '</div>'; } } /** * The SUT with a really small refactoring, which does not break its Api. * Of course in reality it would have the same name of the original SUT... */ class TestableTimeBox { public function __toString() { return '<div class="current_timestamp">' . date('Y-m-d', $this->currentTime()) . '</div>'; } protected function currentTime() { return time(); } } /** * A Test-Specific Subclass that overrides the Test Hook. This is *not* part * of production code. * You add all these lines of code specific just in order to run a simple test: * the payback you get is the shorter test length. A next step could be to * inject a simple collaborator wrapping time(). */ class TestSubclassOfTestableTimeBox extends TestableTimeBox { public function __construct($currentTime) { $this->currentTime = $currentTime; } protected function currentTime() { return $this->currentTime; } }
Opinions expressed by DZone contributors are their own.
Comments