Software Alchemist

I am constantly seeking answers and ways of transforming desires into reality by coding

Symfony2 Controller Testing

| Comments

Having a rich background in different MVC frameworks, one thing that was always unclear was how to test controllers. I think the main reason it wasn’t obvious is because controllers had always been sort of a black magic element in frameworks. There were too many conventions on where on the file system controller should be located, what dependencies it should have knowledge of and those dependencies were always wired with the controller the hard way (view layer).

Environment like that assumes no easy way for controller testing, since you can’t instantiate a controller and some of its primary dependencies to test the interaction - you have to boot the whole framework and write functional tests.

Because of how complicated that process is, people usually don’t unit-test controllers, functional test is the maximum you can get, but usually you don’t get anything at all.

Symfony2 changes that completely.

Initially Symfony2 framework had only conventional controller loading approach. Controller instance still was very lightweight and was not required to extend some parent class in order to work. If your controllers implemented ContainerAware interface, you would have the DIC (dependency injection container) inserted using ContainerAware::setContainer() method, which you could then use to access any service that you registered in DIC.

The proposed method of testing controllers at the time was a black box testing approach, where you test full requests to your application and assert their output like so:

1
2
3
4
5
6
7
8
9
<?php

$client = $this->createClient();
$client->request('GET', '/index');

$response = $client->getResponse();

$this->assertEquals(200, $response->getStatusCode());
$this->assertRegExp('/<h1>My Cool Website<\/h1>/', $response->getContent());

Although this method is easy to read and understand, there are a couple of drawbacks:

  • To execute the test, we need to bootstrap the Kernel
  • This test can only assert response body, which makes it fragile to design changes
  • As a result of all of the above, it runs much slower and does much more than it needs to

In ideal world, I would want to test controller interaction with other services in my application, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace Company\ApplicationBundle\Tests\Controller;

use Company\ApplicationBundle\Controller\IndexController;

class IndexControllerTest extends \PHPUnit_Framework_TestCase
{
    //...
    public function testIndexAction()
    {
        $templating = $this->getMockTemplating();
        $templating->expects($this->once())
            ->method('render')
            ->with('ApplicationBundle:Index:index')
            ->will($this->returnValue('success'))
        ;

        $controller = new IndexController();
        $controller->setTemplating($templating);

        $this->assertEquals('success', $controller->indexAction());
    }

    private function getMockTemplating()
    {
        return $this->getMock('Symfony\Bundle\FrameworkBundle\Templating\Engine', array(), array(), '', false, false);
    }

}

note Controller is just a POPO (plain old PHP object) without any base class that it needs to extend. Symfony2 doesn’t need anything but the class itself for controller to work.

note read more about mock object framework in PHPUnit

Well, the good news is that Symfony2 allows that. Now all your controllers can be services. The old, conventional approach is still supported and is irreplaceable for small application controllers, with no need for unit-testing.

To make the above example controller correctly interact with Symfony2 and work as expected, we need the following.

Create controller class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace Company\ApplicationBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Templating\Engine;

class IndexController
{
    /**
     * @var Symfony\Bundle\FrameworkBundle\Templating\Engine
     */
    private $templating;

    /**
     * @param Symfony\Bundle\FrameworkBundle\Templating\Engine $templating
     */
    public function setTemplating(Engine $templating)
    {
        $this->templating = $templating;
    }

    /**
     * @return Symfony\Component\HttpFoundation\Response
     */
    public function indexAction()
    {
        return $this->templating->render('ApplicationBundle:Index:index');
    }
}

Create DIC configuration, using the following xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="index_controller" class="Company\ApplicationBundle\Controller\IndexController">
            <call method="setTemplating" />
                <argument type="service" id="templating" />
            </call>
        </service>
    </services>
</container>

Create routing configuration:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://www.symfony-project.org/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.symfony-project.org/schema/routing http://www.symfony-project.org/schema/routing/routing-1.0.xsd">

    <route id="index" pattern="/index">
        <default key="_controller">index_controller:indexAction</default>
    </route>
</routes>

note in the above example, we used service_id:action instead of the regular BundleBundle:Controller:action (without the “Action” suffix)

After all of the above is done, we need to inform Symfony2 of our services. To avoid creating a new Dependency Injection extension and creating configuration file entry, we can register our services directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace Company\ApplicationBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class ApplicationBundle extends Bundle {
    public function registerExtensions(ContainerBuilder $container) {
        parent::registerExtensions($container);

        // register controllers
        $loader = new XmlFileLoader($container);
        $loader->load(__DIR__.'/Resources/config/controllers.xml');
    }
}

note the above technique was originally invented by Kris Wallsmith in a project we were working on at OpenSky.

Now you’re ready to go. You need to include your bundle level routing file in the application level routing configuration, create the Index directory. The final file structure should look something like this:

Company
| - ApplicationBundle
|   | - Controller
|   |   | - IndexController.php
|   | - Resources
|   |   | - config
|   |   |   | - controller_routing.xml
|   |   |   | - controllers.xml
|   |   | - views
|   |   |   | - Index
|   |   |   |    | - index.php
|   | - ApplicationBundle.php

After all the above steps are completed, you can try it from the browser, by going to:

http://your_application/your_front_controller.php/index

Happy Coding!

Comments