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!

PHP Symfony2 Testing PHPUnit controller functional testing unit testing
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Shiva kumar

I need to do mock test for a webservice . Unable to inject response data

oscherler

So by functional testing the controllers, do you mean the following:

I have an action that:

– extracts parameters from the request (either GET parameters, by binding a form, or even from the session);
– then calls a service with those parameters to perform the business logic;
– finally renders a template or performs a redirect.

So I would:

– simulate a request using the web client;
– mock my business logic service with:
    • expectations on the parameters it should receive (testing that the controller extracts the paramters and calls the service properly);
    • making it return appropriate data (so I can test the second half of the controller action);
– and test that the response is consistent with the data returned by the mocked service.

And for testing the actual service, I use unit tests.

Does this seem like a reasonable thing to do?

Silviu

I had a similiar problem, when I try to spec my controllers with phpspec 2.x, and it works, I mean I could use stubs and mocks for form, router, session, any other colaborators for my controller but thing stop when in my controller I should test/spec security, simply get security.context doesn't work because it's has all fields null. So because of this my spec for the controller now it breaks.
I google it about test and symfony2 and I get over this presentation from slideshare, by Fabien: http://www.slideshare.net/f... where at 16 it says: " Do not write Unit Test for a Controller".

Matthias Noback

Right, as you can see in the other comments, I've changed my mind about this too. Controllers should be really thin, and you can test its behavior better using a functional or acceptance test.

Emelec tricampeón!!

In conclusion, unit tests should be used just for services, right?, so does all business logic have to be written in services?

Matthias Noback

There's much more to it, so I wouldn't reduce it to "only unit test services". But at least, don't unit test controllers ;)

Silviu

I saw this, I just explaining my problem https://groups.google.com/f... :) I hope, you don't mind.

Matthias Noback

I see! No problem, I think I was also repeating myself here, since I almost wrote exactly the same two comments above.

Boyer

Try Goutte & Crawler & CSS Selector & Webcase please ;)

Matthias Noback

Right! As you can read in the comments I already changed my mind.

Carles Climent

IMO, fine-grained controller testing has no sense.

Controllers should be slim and have NO BUSINESS LOGIC at all. All the fine-grained testing should be derived to model classes.

If you want to verifiy that the method 'bind' is being called, go on, but these are tests that provide questionable value.

Test user interaction with functional tests, and bussines logic with unitary tests.

Carles Climent

Sorry, didn't read all the comments.

Nice to see you arrived to the same conclusion ;)

Gustavo

Hello there,
Thank you for the illustration.
We are currently unit-testing controller on our Sf2 app, but we get some blockers because the most of the controllers use several services, and it is so hard to mock them. So, how would you suggest to proceed here?, We've researched over the web but all of them advise to use functional test, so the services initialization is managed by the framework itself.
Thanks in advance

Matthias Noback

Hi Gustavo,

After some discussion (see above) I have decided that testing the controller "stand-alone" is not the way. You should have all clean unit-testable code in some other place. The controller should only hook things up. This integration of components should be tested using a functional test.
(In my opinion that is ;))

Best regards,

Matthias

James R Grinter

The challenge is that Symfony makes it really hard for non-Controllers to access everything it has set up, without lots of dependency injection "wiring" all over the place.

So Controllers end up with lots of non-controller logic.

Matthias Noback

A nice alternative: http://www.whitewashing.de/...
Symfony actually makes it very easy to access everything - dependency wiring is what developing Symfony applications is all about. When using too many services inside one controller, maybe split the controller into multiple controllers (chances are you are giving it too many responsibilities), or combine services in related sets of services (like in the article I mentioned above).

ezonno

I always test my controllers via functional tests, and test the services with individual unit tests. Another great mock / stub framework is Mockery. https://github.com/padraic/...

Lukas

What is the benefit of this unit test over a functional test? The functional test is obviously going to be slower. But it will also test the configuration and availability of services. It will also catch any issue your unit test will cover. And I bet if there is an issue in the logic you would find it just as quickly with the functional as with the unit test. But if you change any of your services you will need to rewrite 10-20 LOC of mock configs. Not the kind of thing that makes we want to refactor, which is what unit test should in theory make easier .. not harder.

Daniel Holmes

It will also catch any issue your unit test will cover
There are cases where I don't think this is true, especially in the area of exception handling. Consider the case of confirming a registration, from your controller you might call $confirmationService->confirm('email@example.org'). This application service might have several exceptions - EmailNotRegisteredException, EmailAlreadyConfirmedException, EmailBannedException, etc that all simply pass a different error message to your template. A thorough functional test suite of such a controller might take 10 seconds whereas the unit equivalent with maybe one functional test would be a couple of seconds at most. There's also the problem of how to fake service responses, e.g. in the blog post example you might want to write a handler for exceptions during sending - easily fakable in unit tests but not as easy in functional.

But if you change any of your services you will need to rewrite 10-20 LOC of mock configs
That's a possible problem for any unit tests - how to effectively setup your stubs/mocks. I don't think Controllers are special in this regard - they are still just a class taking an input, interacting with services and generating an output. I find PHPUnit's mocks especially bad for mock config because it combines the Arrange and Assert steps. I'm a big fan of Phake in this regard: https://github.com/mlively/...

Matthias Noback

Hi Lukas and Daniel,
I was hoping for some strong advise on this matter - thank you for providing it! Testing a controller with unit tests allows you much more fine-grained tests. This way you may cover all execution paths of the code. Then, using functional tests, you may (and should) test the flow which is most common for your users, to verify that they (real persons or webservice clients) are able to interact with your application.
Thanks for your suggestion about Phake, Daniel, I will give it a try.