Getting started with PhpSpec and Symfony 2
This is the first post in a series of posts that will take you through a step by step guide to getting started with PhpSpec and Symfony 2. If you want to skip straight to a better approach for testing controllers check out my second post in this series.
This first post is going to look at how a controller works within Symfony 2 and how we might begin to start writing specifications for one (as such the following examples should not be used in practice). Subsequent posts will refactor these examples incorporating some techniques for better application structure.
To follow these examples you will need to install the Symfony 2 standard edition with PhpSpec. I have created a repository with all the example code and you can follow the commits step by step.
Lets start with a familiar example of a Controller that you may create when you are getting started with Symfony 2 and following the examples in the documentation.
namespace Peterjmit\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function indexAction()
{
$repository = $this->getDoctrine()
->getManager()
->getRepository('PeterjmitBlogBundle:Blog');
$posts = $repository->findAll();
return $this->render(
'PeterjmitBlogBundle:Blog:index.html.twig',
array('posts' => $posts)
);
}
}
Using PhpSpec we can create a spec file for our BlogController
$ ./bin/phpspec describe Peterjmit/BlogBundle/Controller/BlogController
The next step is to start specifying behaviours for our controller by adding
methods to our Spec class, and as we already have an indexAction
in
BlogController
, we can start trying to describe the behaviour of that action.
// spec/Peterjmit/BlogBundle/Controller/BlogControllerSpec.php
// ...
function it_should_respond_to_index_action()
{
$this->indexAction();
}
If we try and run that spec, we will be greeted with a fatal PHP error:
Fatal error: Call to a member function has() on a non-object in [...]Symfony/Bundle/FrameworkBundle/Controller/Controller.php on line 198
At this point you probably understand how to write a Symfony controller, but exactly how it works may not be clear. To start specifying its behaviour we need to understand how to properly isolate it, what its dependencies are, and what behaviour it needs to fulfil as part of a Symfony 2 based application.
What behaviour does Symfony 2 require from a controller?
This question is easy to answer, and it is one of my favourite things about the Symfony 2 framework (suggested reading: Requests, Controller, Response Lifecycle). Because all of the framework components are decoupled, very little happens needs to happen within a controller, and this can make it very easy to test.
At a basic level a controller within Symfony2 is any PHP callable that returns
an instance of the Symfony2 Response object. In the standard edition a
controller is usually a method postfixed with Action
located within a
Controller class.
Based on this information we can develop the initial spec we wrote for
indexAction
a little further:
// spec/Peterjmit/BlogBundle/Controller/BlogControllerSpec.php
// ...
function it_should_respond_to_index_action()
{
$response = $this->indexAction();
$response->shouldHaveType(
'Symfony\Component\HttpFoundation\Response'
);
}
The test will still fail however, because we have not yet worked out a way to
fulfil the dependencies that our BlogController
has so that we don't get
any more fatal errors.
Understanding the dependencies of your controller
In order to test our Controller, we need a way of fulfilling the dependencies it has at runtime. In PhpSpec the strategy for achieving this involves isolating the Subject Under Specification (SUS) so that our specification only deals with its behaviour.
If we revisit the controller that we created at the beginning you will see
that we have extended the Symfony\Bundle\FrameworkBundle\Controller\Controller
.
This class provides some convenience methods that let you write some common
controller actions more quickly. However from a testing point of view it means
we have a bunch of unspecified behaviours that we need to define.
What is a ContainerAware
controller?
To put it simply, if your controller is ContainerAware
Symfony 2 will make
sure that the dependency injection container is available in your controller.
The framework bundle controller we have extended implements ContainerAwareInterface
(via the abstract class ContainerAware
). When Symfony 2 resolves a
controller during the Requests, Controller, Response Lifecycle, it will call
setContainer
with an instance of the dependency injection container
(a class implementing ContainerInterface
) on any controller that
implements ContainerAwareInterface
.
Therefore we need to recreate this condition in our specification and we can
do this via PhpSpec's let
method:
// spec/Peterjmit/BlogBundle/Controller/BlogControllerSpec.php
// ...
use Symfony\Component\DependencyInjection\ContainerInterface;
class BlogControllerSpec extends ObjectBehavior
{
function let(ContainerInterface $container)
{
$this->setContainer($container);
}
// ...
If we try and run the specification now, we will no longer see a PHP fatal error
(yay!), instead PhpSpec will be complaining that an exception has been thrown.
This is where we can start faking, stubbing, mocking and spying on the behaviour
of the collaborators that BlogController
relies on.
Stubbing the interaction with Doctrine
The first method we use from the framework controller is getDoctrine
to
get repository for our Blog entity. If you take a look at the method you will see
that it checks the container to see if the doctrine service is registered,
and returns it if it is.
In order to mock the repository (and get a list of blog posts) we need to understand the collaborators that are involved in retrieving it from the doctrine service, here is a list:
Doctrine\Common\Persistence\ManagerRegistry#getManager
Doctrine\Common\Persistence\ObjectManager#getRepository
Doctrine\Common\Persistence\ObjectRepository#findAll
(check out the source for the interfaces, note if you are using custom
repository methods you will need to use that class in the spec instead of
ObjectRepository
otherwise PhpSpec will complain)
Just as we stubbed the container, we can stub all of these methods, and
define some more behaviour for indexAction
:
// spec/Peterjmit/BlogBundle/Controller/BlogControllerSpec.php
// ...
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Persistence\ObjectRepository;
class BlogControllerSpec extends ObjectBehavior
{
function let(
ContainerInterface $container,
ManagerRegistry $registry,
ObjectManager $manager,
ObjectRepository $repository
) {
$container->has('doctrine')->willReturn(true);
$container->get('doctrine')->willReturn($registry);
$registry->getManager()->willReturn($manager);
$manager
->getRepository('PeterjmitBlogBundle:Blog')
->willReturn($repository);
// ...
}
// ...
function it_should_respond_to_index_action(
ObjectRepository $repository
) {
// findAll could return an array of blog post entities,
// but we are not interested in the return value of findAll
// because it does not influence the behaviour of our
// controller in this example
$repository->findAll()->willReturn(array());
$response = $this->indexAction();
$response->shouldHaveType(
'Symfony\Component\HttpFoundation\Response'
);
}
}
At this point it is worth noting that it was somewhat of a hassle to set up all the collaborators involved for getting a repository. When something is difficult or inconvenient to test it is an indicator that you need to look at a different way of achieving the behaviour.
In this example our controller knows too much about the implementation of retrieving blog posts, and refactoring should aim to reduce the number of collaborators - my next blog post will discuss how we can do this in a Symfony application.
Stubbing templating
The final method we use from the framework bundle controller is render
.
This method is a proxy method to the renderResponse
method on the templating
service which in the Symfony 2 standard edition is an instance of
EngineInterface
.
When looking at the behavour of renderResponse
you will see that it returns
a Response
object which will conveniently fulfil our initial specificiation
for the behaviour of indexAction
.
The specification for mocking/stubbing templating (and the Response object) looks like this:
// spec/Peterjmit/BlogBundle/Controller/BlogControllerSpec.php
// ...
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
class BlogControllerSpec extends ObjectBehavior
{
function let(
// ...
EngineInterface $templating
) {
// ...
$container->get('templating')->willReturn($templating);
// ...
}
// ...
function it_should_respond_to_index_action(
// ...
EngineInterface $templating,
Response $mockResponse
) {
// ...
$templating
->renderResponse(
'PeterjmitBlogBundle:Blog:index.html.twig',
array('posts' => array()),
null
)
->willReturn($mockResponse)
;
// ...
}
The final code for our specification can be seen in the github repository I mentioned at the beginning. It is important to note that we didn't have to define any routing, write any entities (or mapping) to write a valid controller. This is part of the beauty of a BDD approach is that it allows you to focus on one cog in the machine at a time.
This is not the right way™
As with many complex topics in education, you need to un-learn what you were taught at the beginning to get to the next level. Hopefully you will realise is that writing the above specifications and implementations for every controller in your application is rather cumbersome and will create a lot of repetition in your code base.
Advice for solving some of the problems introduced by the above examples is outside of the scope of this initial post but we will cover it next time. Until then here are some good resources that should begin to describe the problems and suggest some solutions.
- From STUPID to SOLID Code!
- Symfony2: Controller as Service
- Extending Symfony2: Controller Utilities
- Putting your Symfony2 controllers on a diet, part 2
Some additional resources for PhpSpec
- phpspec2: SUS and collaborators
- An explanation for fakes, stubs, mocks and spies
- A start to writing a Symfony2 extension for PhpSpec
Please get in touch with me on twitter if you have any comments, or fork this blog post and contribute!
Follow @peterjmitShare on twitter