Testing ZF2 module services

by Mateusz Tymek — on Zend Framework, PHP

There's an important question often rising when working on Zend Framework 2 module: should I test service factories? After all, they are usually trivial, they create some object and inject it with dependencies from ServiceManager. Having one test per factory seems to be an overkill.

Better to go one step back, and ask yourself a question: what exactly do you want to test?

To better illustrate this problem, let's consider two services available from your module:

namespace MyModule\Service;

class FooService
{
    public function __construct()
    {
    }
}

class BarService
{
    /**
     * @var FooService
     */
    private $fooService;

    public function __construct(FooService $fooService)
    {
        $this->fooService = $fooService;
    }
}

FooService is a simple class with no dependencies, and it is required by BarService. We need to have a factory for BarService:

namespace MyModule\Factory;

use MyModule\Service\BarService;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class BarServiceFactory implements FactoryInterface
{
    /**
     * @inheritdoc
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        return new BarService(
            $serviceLocator->get('MyModule\Service\FooService')
        );
    }
}

This code is trivial, and FooService is type hinted in constructor, so one could argue that writing unit test for factory itself is pointless. What is an alternative?

Back to question: "what exactly do you want to test?". Typically, services in your module are exposed using configuration file, which should be treated as kind of module interface. Then, it is worth to test if your module really exposes this services, no matter how they are created or what dependencies do they have. Take a look at example configuration that provides two services defined above:

return [
    'service_manager' => [
        'invokables' => [
            'MyModule\Service\FooService' => 'MyModule\Service\FooService',
        ],
        'factories' => [
            'MyModule\Service\BarService' => 'MyModule\Factory\BarServiceFactory',
        ],
    ]
];

If you follow convention of having service name exactly the same as returned class name, your test can easily iterate over configuration, pull every service from ServiceManager, and validate it:

namespace MyModuleTest;

use MyModule\Module;
use PHPUnit_Framework_TestCase;

class ModuleTest extends PHPUnit_Framework_TestCase
{
    /**
     * Scans service manager configuration, returning all services created by factories and invokables
     * @return array
     */
    public function provideServiceList()
    {
        $config = include __DIR__ . '/../../config/module.config.php';
        $serviceConfig = array_merge(
            isset($config['service_manager']['factories'])?$config['service_manager']['factories']:array(),
            isset($config['service_manager']['invokables'])?$config['service_manager']['invokables']:array()
        );
        $services = array();
        foreach ($serviceConfig as $key => $val) {
            $services[] = array($key);
        }
        return $services;
    }

    /**
     * @dataProvider provideServiceList
     */
    public function testService($service)
    {
        $sm = Bootstrap::getServiceManager();
        // test if service is available in SM
        $this->assertTrue($sm->has($service));
        // test if correct instance is created
        $this->assertInstanceOf($service, $sm->get($service));
    }
}

This approach has several advantages:

  • you ensure that your module correctly exposes everything defined in configuration
  • this test will catch every typo or notice you may have in any factory
  • you get 100% test coverage over all factories
  • you don't waste time on writing useless tests

Note that you'll likely need to adjust above code, to match your module specifics. You can use it as a base to test controller plugins or view helpers as well.

About non-trivial factories

Just to make sure: I'm not against testing factories in every situation! When your factory does something more complex (like creating and injecting adapters based on configuration), you should obviously create separate test case.

Running tests

In order to run this code, you'll need a bootstrapper that sets up ZF2 application and your module. Official documentation has a chapter about unit testing - you can take sample code and alter it to match your needs:

<?php
namespace MyModuleTest;

use Zend\Loader\AutoloaderFactory;
use Zend\Mvc\Application;
use Zend\Mvc\Service\ServiceManagerConfig;
use Zend\ServiceManager\ServiceManager;
use RuntimeException;

error_reporting(E_ALL | E_STRICT);
chdir(__DIR__);

/**
 * Test bootstrap, for setting up autoloading
 */
class Bootstrap
{
    protected static $serviceManager;

    public static function init()
    {
        $zf2ModulePaths = array(dirname(dirname(__DIR__)));
        if (($path = static::findParentPath('vendor'))) {
            $zf2ModulePaths[] = $path;
        }
        if (($path = static::findParentPath('module')) !== $zf2ModulePaths[0]) {
            $zf2ModulePaths[] = $path;
        }

        static::initAutoloader();

        // use ModuleManager to load this module and it's dependencies
        if (file_exists(__DIR__ . '/TestConfiguration.php')) {
            $config = require __DIR__ . '/TestConfiguration.php';
        } else {
            $config = require __DIR__ . '/TestConfiguration.php.dist';
        }

        $serviceManager = new ServiceManager(new ServiceManagerConfig());
        $serviceManager->setService('ApplicationConfig', $config);
        $serviceManager->get('ModuleManager')->loadModules();

        $application = new Application($config, $serviceManager);
        $application->bootstrap();

        static::$serviceManager = $serviceManager;
    }

    public static function getServiceManager()
    {
        return static::$serviceManager;
    }

    protected static function initAutoloader()
    {
        $vendorPath = static::findParentPath('vendor');

        $zf2Path = getenv('ZF2_PATH');
        if (!$zf2Path) {
            if (defined('ZF2_PATH')) {
                $zf2Path = ZF2_PATH;
            } elseif (is_dir($vendorPath . '/ZF2/library')) {
                $zf2Path = $vendorPath . '/ZF2/library';
            } elseif (is_dir($vendorPath . '/zendframework/zendframework/library')) {
                $zf2Path = $vendorPath . '/zendframework/zendframework/library';
            }
        }

        if (!$zf2Path) {
            throw new RuntimeException(
                'Unable to load ZF2. Run `php composer.phar install` or'
                . ' define a ZF2_PATH environment variable.'
            );
        }

        if (file_exists($vendorPath . '/autoload.php')) {
            include $vendorPath . '/autoload.php';
        }

        include $zf2Path . '/Zend/Loader/AutoloaderFactory.php';
        AutoloaderFactory::factory(array(
            'Zend\Loader\StandardAutoloader' => array(
                'autoregister_zf' => true,
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/' . __NAMESPACE__,
                ),
            ),
        ));
    }

    protected static function findParentPath($path)
    {
        $dir = __DIR__;
        $previousDir = '.';
        while (!is_dir($dir . '/' . $path)) {
            $dir = dirname($dir);
            if ($previousDir === $dir) {
                return false;
            }
            $previousDir = $dir;
        }
        return $dir . '/' . $path;
    }
}

Bootstrap::init();
Now prepare simple configuration file telling ZF to load your module (add dependencies if it has any):
<?php
return array(
    'modules' => array(
        'MyModule',
    ),
    'module_listener_options' => array(
        'config_glob_paths' => array(),
        'module_paths' => array(),
    ),
);

Finally, PHPUnit XML configuration (phpunit.xml):

<phpunit bootstrap="Bootstrap.php">
    <testsuite name="MyModuleTest">
        <directory>MyModuleTest</directory>
    </testsuite>
    <filter>
        <whitelist addUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">../src</directory>
        </whitelist>
        <blacklist>
            <directory>../vendor</directory>
        </blacklist>
    </filter>
</phpunit>

Github example

I prepared working demo that can be downloaded from Github. Go to example.


comments powered by Disqus