PHPUnit & Pimple: Integration Tests with a Simple DI Container

Posted on by Matthias Noback

Unit tests are tests on a micro-scale. When unit testing, you are testing little units of your code to make sure that, given a certain input, they produce the output you expected. When your unit of code makes calls to other objects, you can "mock" or "stub" these objects and verify that a method is called a specific number of times, or to make sure the unit of code you're testing will receive the correct data from the other objects.

But when you have a nice landscape of classes, which are all very well tested, you have to move up a bit on the testing pyramid and test some of these classes when they are working together. This usually requires some setup. Say you have some class, called a BatchProcessor, that works directly with a Doctrine\DBAL\Connection object. You may have a nice set of unit tests for it, to verify that the right queries are executed. But you also want to have a test which really integrates your class with Doctrine DBAL and with a real database (even though it would be an in-memory SQLite database). The best way to accomplish this is to create and configure the Connection class in the setUp() method of your test class:

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Schema\Schema;

class BatchProcessorIntegrationTest extends \PHPUnit_Framework_TestCase
{
    private $connection;
    private $batchProcessor;

    protected function setUp()
    {
        $this->connection = DriverManager::getConnection(
            array(
                'driver' => 'pdo_sqlite',
                'memory' => true,
            )
        );

        $schema = new Schema();

        $table = $schema->createTable('users);
        $table->addColumn('name', 'string');
        ...

        foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) {
            $this->connection->exec($sql);
        }

        $this->batchProcessor = new BatchUpdater($this->connection);
    }

    public function testProcessSomething()
    {
        $this->batchProcessor->...;
    }

    protected function tearDown()
    {
        $this->connection->close();

        // to help the garbage collector:
        $this->connection = null;
        $this->batchProcessor = null;
    }
}

With a test like this, you can assure yourself that things will really work, once your code is integrated with Doctrine DBAL. However, soon you will create a second test class in which you would be happy to use the same code for setting up a connection and a schema. You could choose to create some kind of an abstract test class, but then each test class is required to extend from this base class. It would be cleaner (and better maintainable) if you could reuse creation logic required by your test class by having some kind of an external service container from which you could fetch whatever object you needed. Let's first set up some classes and interfaces for the desired functionality and then continue with some more advanced examples.

A simple service container

Reusing creation logic almost immediately brings my mind to the concept of a dependency injection or service container. It need not be very advanced though, since it will just execute from within a unit test. I chose Pimple for this, since it is very well suited for the purpose: defining an entire object graph with dependencies, support for extending services and for keeping plain parameters.

We will have a generic service container, extending the Pimple dependency injection container, and service providers, which can be used to register services on the fly. These are their interfaces:

interface ServiceContainerInterface
{
    public function registerProvider(ServiceProviderInterface $serviceProvider);
}

interface ServiceProviderInterface
{
    public function register(\Pimple $container);
}

A very basic implementation of a service container would be:

class ServiceContainer extends \Pimple implements ServiceContainerInterface
{
    public function registerProvider(ServiceProviderInterface $provider)
    {
        $provider->register($this);
    }
}

Service providers

Now each service provider is allowed to register services and parameters using the container. For example this would be the ConnectionProvider:

use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;

class ConnectionProvider implements ServiceProviderInterface
{
    public function register(\Pimple $container)
    {
        // set parameters for creating the connection
        $container['connection_options'] = array(
            'driver' => 'pdo_sqlite',
            'memory' => true,
        );

        // define the "connection" service
        $container['connection'] = $container->share(
            function(\Pimple $container) {
                return DriverManager::getConnection(
                    $container['connection_options']
                );
            }
        );

        // extend the "connection" service to also load the schema, when defined
        $container['connection'] = $container->extend('connection',
            function(Connection $connection, \Pimple $container) {
                if (!isset($container['schema'])) {
                    return $connection;
                }

                $schema = $container['schema'];
                $databasePlatform = $connection->getDatabasePlatform();

                foreach ($schema->toSql($databasePlatform) as $sql) {
                    $connection->exec($sql);
                }

                return $connection;
            }
        );
    }
}

We also need a BatchProcessorProvider to create an instance of BatchProcessor using the connection service:

class BatchProcessorProvider implements ServiceProviderInterface
{
    public function register(\Pimple $container)
    {
        $container['batch_processor'] = $container->share(
            function(\Pimple $container) {
                new BatchProcessor($container['connection');
            }
        );
    }
}

A test case which uses the service container

With these service providers and the service container itself in place, we can have the following clean setup in our test class:

class BatchProcessorIntegrationTest extends \PHPUnit_Framework_TestCase
{
    private $container;

    protected function setUp()
    {
        $this->container = new ServiceContainer();
        $this->container->registerProvider(new ConnectionProvider());
        $this->container->registerProvider(new BatchProcessorProvider());

        $schema = new Schema();
        $table = $schema->createTable('users);
        $table->addColumn('name', 'string');
        ...

        $this->container['schema'] = $schema;
    }

    public function testProcessSomething()
    {
        $this->container['batch_processor']->...;
    }

    protected function tearDown()
    {
        $this->container['connection']->close();

        $this->container = null;
    }
}

A terminable service container

It may be well worth it to make the implementation a bit more transparent even. The first step would be to give both the service container and the service providers a method terminate() which can be called to clean things up.

interface ServiceContainerInterface
{
    public function registerProvider(ServiceProviderInterface $serviceProvider);

    public function terminate();
}

interface ServiceProviderInterface
{
    public function register(\Pimple $container);

    public function terminate(\Pimple $container);
}

Then inside the ServiceContainer we keep track of the available service providers and terminate them one by one:

class ServiceContainer extends \Pimple implements ServiceContainerInterface
{
    private $providers = array();

    public function registerProvider(ServiceProviderInterface $provider)
    {
        $this->providers[] = $provider;
        $provider->register($this);
    }

    public function terminate()
    {
        foreach ($this->providers as $provider) {
            $provider->terminate($this);
        }
    }
}

The ConnectionProvider uses its terminate() method just to close the connection:

class ConnectionProvider
{
    ...

    public function terminate(\Pimple $container)
    {
        $container['connection']->close();
    }
}

The tearDown() method in the test class now becomes:

protected function tearDown()
{
    $this->container->terminate();

    $this->container = null;
}

Required arguments for service providers

As we saw above, the ConnectionProvider defines a connection service. This service is then extended to make sure a given Schema is transformed to SQL statements which are then executed. Since this may be something you always want to be taken care of, you could say that providing a schema service, which is in fact a Schema object, is mandatory when you want to use the ConnectionProvider. This requires the following changes to the ConnectionProvider class:

class ConnectionProvider implements ServiceProviderInterface
{
    private $schema;

    public function __construct(Schema $schema)
    {
        $this->schema = $schema;
    }

    public function register(\Pimple $container)
    {
        $container['schema'] = $this->schema;
    }
}

Then the setUp() method of your test class becomes:

protected function setUp()
{
    $schema = new Schema();
    $table = $schema->createTable('users);
    $table->addColumn('name', 'string');
    ...

    $this->container = new ServiceContainer();
    $this->container->registerProvider(new ConnectionProvider($schema));
    $this->container->registerProvider(new BatchProcessorProvider());
}

Continuing this idea...

I have been using this method of decoupling the creation of object graphs from unit tests and it's been very helpful in keeping test classes clean and making creation logic very portable and thereby reusable.

There are of course many more situations you could think of when using a simple dependency injection container like Pimple helps keeping your mind straight.

PHP Testing dependency injection PHPUnit Pimple service container reusability