Stub ParamConverter in phpunit

This week I spent exactly 5 hours and 38 minutes trying to stub what Sensio’s ParamConverter would return in my tests…

So I thought it would be a good idea to share what I learned on this journey and how I managed to do it.

If you have a better way of doing this: please come forward 🙏 because my solution feels kind of hacky.

For more information on ParamConverter, checkout the official documentation: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html

Basically what ParamConverter does is automatically fetch an object for you without having to deal with EntityManagers.

The code we want to test

Suppose you have a UserController.php with a show action:

/**
* @Route('/user/{id}', name='show_user')
*/
public function showAction(User $user)
{
return $this->render(
'user/show.html.twig', [ 'user' => $user ]
);
}

when you call the URL : myawesomeproject.com/user/1, Symfony will automatically fetch the user with the id 1 from the database.

The tests

Suppose now you want to test the fact that the page actually renders successfully (http 200 status).

BUT In order to keep our tests fast we don’t want them to actually fetch anything from the database. So we create a fake user and feed it to our stubbed ParamConverter.

We create a UserControllerTest.php

class UserControllerTest extends WebTestCase 
{
function setUp()
{
$this->client = static::createClient();
}
    function testItRendersShowPage()
{
// Insert something to tell phpunit not fetch the real user
// from the database

$crawler = $this->client->request('GET', '/user/1');
$this->assertEquals(
200, $this->client->getResponse()->getStatusCode()
);
}
}

Here comes the hard part. When a request is made, a listener (ParamConverterListener) checks whether a param has to be converted. If there is one, a ParamConverterManager returns a list of available ParamConverters which is looped over to find one that supports this type of parameter. If one is found (DoctrineParamConverter in our case), it fires a ManagerRegistry that will call the appropriate ObjectManager to fetch the entity from the database thanks to its EntityRepository. wow….

Here is a diagram to visualize things a bit:

The worst part is we need to stub ALL of these classes 😣

Let’s do it:

First we create our fake user:

$user = new User();

then we mock its EntityRepository, and stub its find method to return our fake user:

$entityRepository = $this->getMockBuilder(EntityRepository::class)
->disableOriginalConstructor()
->getMock();
$entityRepository->expects($this->any())
->method('find')
->willReturn($user);

then we mock the ObjectManager, and stub its getRepository method to return our mocked EntityRepository:

$objectManager = $this->getMockBuilder(ObjectManager::class)
->disableOriginalConstructor()
->getMock();
$objectManager->expects($this->any())
->method('getRepository')
->willReturn($entityRepository);

then we mock the ManagerRegistry, and stub its getManagerForClass method to return our mocked ObjectManager:

$managerRegistry = $this->getMockBuilder(ManagerRegistry::class)
->setMethods(array('getManagerForClass'))
->disableOriginalConstructor()
->getMockForAbstractClass();
$managerRegistry->expects($this->any())
->method('getManagerForClass')
->willReturn($objectManager);

then we mock the DoctrineParamConverter class, and stub its __construct and supports methods to make it use our mocked ManagerRegistry:

$doctrineParamConverter = 
$this->getMockBuilder(DoctrineParamConverter::class)
->setMethods(array('__construct', 'supports'))
->setConstructorArgs(array($managerRegistry))
->getMock();
$doctrineParamConverter
->method('supports')
->willReturn(true);

then we mock the ParamConverterManager class, and stub its add and all methods to return an array of available ManagerRegistry with our mocked DoctrineParamConverter class:

$paramManager = $this->getMockBuilder(ParamConverterManager::class)
->setMethods(array('add', 'all'))
->disableOriginalConstructor()
->getMock();
$paramManager->add($doctrineParamConverter, 10);
$paramManager
->method('all')
->willReturn([$doctrineParamConverter]);

finally we mock the ParamConverterListener and stub its __construct method to use our mocked ParamConverterManager:

$paramListener = 
$this->getMockBuilder(ParamConverterListener::class)
->setMethods(array('__construct'))
->setConstructorArgs(array($paramManager))
->getMock();

Last but not least, we tell phpunit to use our mocked ParamConverterListener service:

$this
->client
->getContainer()
->set('sensio_framework_extra.converter.listener', $paramListener);

That’s it for today!

Here is the full UserControllerTest:

Lastly don’t forget to refactor this code in order to make it more reusable.

Happy testing!