Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Practical PHP Testing Patterns: Dependency Lookup

DZone 's Guide to

Practical PHP Testing Patterns: Dependency Lookup

· Web Dev Zone ·
Free Resource

How can we substitute real collaborators with Test Doubles, for the purpose of testing in isolation? We have to build a small and handy indirection over the new operator and in general over the creation of objects.

Usually we can just introduce Dependency Injection: each object ask for its dependencies, and they are injected with their collaborators at construction time.

An alternative to this practice is called Dependency Lookup instead, and it's also known as Service Locator. In this case, the object is injected a Service Locator/Registry/Container, which it pulls its collaborator from.

Pros

If we always use Dependency Injection, why it's useful to know how to work also with this pattern? Because it's simpler to treat with generic code (you can always inject a Locator in each object, regardless of discovering what it really needs.)

As a result, frameworks and library code often prefer this pattern to Dependency Injection. I just spoke with Enrico from the Zend Framework 2 team at a conference, and he told me Service Locator is going to be (with the name of container) the way to make controllers unit testable. Given the age of this issue, this solution is already a lot better than the old approach of having the controllers instantiate their collaborators by themselves.

Another advantage of the pattern is that its further level of indirection may be necessary. For instance, the collaborator may have a shorter lifecycle.

Again, I have a framework example: in Zend Framework 1, View Helpers are small objects which can be used by the view script for displaying snippets of HTML code. They are looked up by the Zend_View object, since creating them all and injecting them into the view object would impact the performance (every time they are not used, they would be created anyway; and there may be dozens of them.) and the complexity of configuration.

PHP is different from Java, where objects like this are created at bootstrap or aynschronously and used to serve many requests. To avoid building an enormous object graph, introduce a Factory that implements the Dependency Lookup, and inject the factory instead of the real collaborator, deferring its instantation.

This problem plagued me where I tried to port Java patterns to PHP, until I realized injecting a Factory was the only solution. Still, the tests are a bit cumbersome since you have to setup stubs that return other stubs: whenever you have a design where you can directly reference the collaborator it all becomes simpler.

The Factory (or Builder, or other creational structure) original role is managing the lifecycle of the collaborators, deciding if and when to call new(). But it also doubles as Service Locator, introduced for the insertion of Test Doubles. By the way, if it's just one or two objects, go for Dependency Injection without thinking too much. When the number of objects to inject is not clear or must be configurable, consider Dependency Lookup.

A last pro for Dependency Lookup is that it's easier to introduce on existing code since it introduces a first seam without too much hassle (having to change all new() statements which involve the SUT, which now require the collaborator to be passed.)

...and cons

Dependency Injection surpass Lookup as the most used pattern for a reason. The SUT grows dependent over the Service Locator, and in general all the code grows dependent over the Service Locator, which is the kitchen sink where all dependencies can be found. Creating more than one Service Locator helps in this case, each dedicated to a different category of client objects.

Still, the code will be unusable without the Service Locator, even if it conceptually required only its collaborator. You cannot detach from the Locator, which is usually a static class or singleton; in the cases where I apply it, I always inject it instead.

The Law of Demeter will always tell you to directly reference the collaborator instead of its Locator. Ultimately this is one of the 'design for testability' patterns which does not really benefit the design of the production code.

Example

The example shows you a good example of this pattern, used for managing view helpers. It's more or less equivalent to Zend Framework implementation, but Zend_View is more complex since it has to look up and load view helpers in many different folders.

My Service Locator here is also not a singleton but an injected Factory: as so, only some code can depend on it (where it is injected) and may also be substituted via a double easily.

<?php
class DependencyLookupTest extends PHPUnit_Framework_TestCase
{
    /**
     * We use the real Locator, since we can change its configuration.
     * It's not totally a unit test since it involves the Locator, but
     * it's often very simple code that would be perfectly replicated by the 
     * first Stub in the chain.
     */
    public function testViewProducesAParagraphAndAnUrl()
    {
        $viewHelperBroker = new ViewHelperBroker(array(
            'p' => 'NullHelper',
            'url' => 'NullHelper'
        ));
        $view = new ViewScript($viewHelperBroker);
        $expected = "Hello World!|"
                  . "\n"
                  . "user|1";
        $this->assertEquals($expected, $view->render());
    }
}

/**
 * A collaborator (a Stub one).
 */
class NullHelper
{
    public function __invoke($arg1 = null, $arg2 = null)
    {
        return $arg1 . '|' . $arg2;
    }
}

/**
 * The SUT.
 */
class ViewScript
{
    private $helperBroker;

    public function __construct(ViewHelperBroker $broker)
    {
        $this->helperBroker = $broker;
    }

    public function render()
    {
        return $this->helperBroker->p()->__invoke('Hello World!')
             . "\n"
             . $this->helperBroker->url()->__invoke('user', '1');
    }
}

/**
 * The Service Locator.
 */
class ViewHelperBroker
{
    private $configuration;

    public function __construct(array $helpersConfiguration)
    {
        $this->configuration = $helpersConfiguration; 
    }

    public function __call($helperName, $arguments)
    {
        $class = $this->configuration[$helperName];
        return new $class;
    }
} 
Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}