PHP & Symfony About PHP and Symfony2 development

Symfony2: Testing Your Controllers

Posted on by Matthias Noback

I consider this article deprecated. Please read the comments: I don't actually think unit testing controllers is a good practice.

Apparently not everyone agrees on how to unit test their Symfony2 controllers. Some treat controller code as the application's "glue": a controller does the real job of transforming a request to a response. Thus it should be tested by making a request and check the received response for the right contents. Others treat controller code just like any other code - which means that every path the interpreter may take, should be tested.

The first kind of testing is called functional testing and can be done by extending the WebTestCase class, creating a client, making a request and crawling the response using CSS selectors. It is indeed very important to test your application in this way - it makes sure all other "glue-like" parts of the system are also in place (like service definitions, configuration parameters, etc.). But it is very, very inefficient. You shouldn't use it to test all possible request-response pairs.

Unit testing your controllers - in my opinion - should be done in the ordinary way, by creating a PHPUnit testcase, instantiating the controller, injecting the necessary dependencies (not the real ones, but stubs/mocks), and finally making some assertions. The easiest way to accomplish this, is by defining the controller as a service, and use setters to provide the controller with it's dependencies.

Creating the sample controller

The controller we will use in this example is a rather simple RegistrationController:

namespace Matthias\RegistrationBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\FormFactoryInterface;
use Matthias\RegistrationBundle\Form\Type\RegistrationType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

/**
 * @Route(service = "matthias_registration.registration_controller")
 */
class RegistrationController
{
    /** @var \Swift_Mailer */
    private $mailer;

    /** @var FormFactoryInterface */
    private $formFactory;

    /** @var EngineInterface */
    private $templating;

    /**
     * @Route("/register", name = "register")
     * @Method({ "GET", "POST"})
     */
    public function registerAction(Request $request)
    {
        $form = $this->formFactory->create(new RegistrationType);

        if ('POST' === $request->getMethod()) {
            $form->bindRequest($request);
            if ($form->isValid()) {
                $message = \Swift_Message::newInstance('Registration', 'Confirm...');

                $this->mailer->send($message);

                return new RedirectResponse('/');
            }
        }

        return new Response($this->templating->render(
            'MatthiasRegistrationBundle:Registration:registration.html.twig', array(
                'form' => $form->createView(),
            )
        ));
    }

    public function setMailer(\Swift_Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setFormFactory(FormFactoryInterface $formFactory)
    {
        $this->formFactory = $formFactory;
    }

    public function setTemplating($templating)
    {
        $this->templating = $templating;
    }
}

As you can see, the service needs the form factory, the templating engine and the mailer. These dependencies should be injected by adding multiple method calls to the service definition:

<service id="matthias_registration.registration_controller" class="Matthias\RegistrationBundle\Controller\RegistrationController">
    <call method="setFormFactory">
        <argument type="service" id="form.factory" />
    </call>
    <call method="setTemplating">
        <argument type="service" id="templating" />
    </call>
    <call method="setMailer">
        <argument type="service" id="mailer" />
    </call>
</service>

Testing the controller

Now, we don't need to extend from the WebTestCase to test this controller. We only need to provide it with the necessary dependencies. The code below demonstrates how to unit test the default flow: create a form, create a view for it, and let the templating engine render it.

use Matthias\RegistrationBundle\Controller\RegistrationController;
use Symfony\Component\HttpFoundation\Request;

class RegistrationControllerTest extends \PHPUnit_Framework_TestCase
{
    public function testRegistrationWithoutPostMethodRendersForm()
    {
        $controller = new RegistrationController;

        $form = $this
            ->getMockBuilder('Symfony\Tests\Component\Form\FormInterface')
            ->setMethods(array('createView'))
            ->getMock()
        ;
        $form
            ->expects($this->once())
            ->method('createView')
        ;

        $formFactory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
        $formFactory
            ->expects($this->once())
            ->method('create')
            ->will($this->returnValue($form))
        ;

        $templating = $this->getMock('Symfony\Component\Templating\EngineInterface');
        $templating
            ->expects($this->once())
            ->method('render')
        ;

        $controller->setFormFactory($formFactory);
        $controller->setTemplating($templating);

        $controller->registerAction(new Request);
    }
}

A few mocks are created, disguised as the objects which the controller expects. The default flow only requires a form factory and a templating engine. The form factory should return a form. Some so-called expectations are added to the mock objects, to make sure that certain methods will be called exactly once.

Note that the test method does not contain any manual assertions. Nevertheless, the expections that were defined, are really assertions, so we have indirectly tested the way the action code interacts with other objects.

Choosing setter injection as the strategy for dependency management really pays off when you are testing more than one controller action, since not all actions will have the same dependencies. For example, when you test the registerAction again, to make sure a mail is sent, you only need the form factory and the mailer:

class RegistrationControllerTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function testRegistrationWithPostMethodValidatesFormAndSendsMailWhenValid()
    {
        $controller = new RegistrationController;

        $request = new Request();
        $request->setMethod('POST');

        $form = $this
            ->getMockBuilder('Symfony\Tests\Component\Form\FormInterface')
            ->setMethods(array('bindRequest', 'isValid'))
            ->getMock()
        ;
        $form
            ->expects($this->once())
            ->method('bindRequest')
            ->with($this->equalTo($request))
        ;
        $form
            ->expects($this->once())
            ->method('isValid')
            ->will($this->returnValue(true))
        ;

        $formFactory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
        $formFactory
            ->expects($this->once())
            ->method('create')
            ->will($this->returnValue($form))
        ;

        $mailer = $this
            ->getMockBuilder('\Swift_Mailer')
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $mailer
            ->expects($this->once())
            ->method('send')
        ;

        $controller->setFormFactory($formFactory);
        $controller->setMailer($mailer);

        $controller->registerAction($request);
    }
}

See also...

Before making this way of testing part of your daily routine, you might want to read up on using stubs (which are stand-in objects that return a controlled value) and mocks (which are used to check that certain methods are called the desired number of times). See the chapter Test doubles in the PHPUnit documentation. It really pays off!

Categories: PHP Symfony2 Testing

Tags: PHPUnit controller functional testing unit testing

Comments: Comments