Tests with the Container
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeUsing a random nickname in a test is weird: we should be explicit about our input and output. Just set it to ObjectOrienter
. Now it's easy to make our asserts more specific, like for the Location
header using assertEquals
, which should be /api/programmers/ObjectOrienter
. And now use the method getHeader()
:
// ... lines 1 - 7 | |
public function testPOST() | |
{ | |
$data = array( | |
'nickname' => 'ObjectOrienter', | |
'avatarNumber' => 5, | |
'tagLine' => 'a test dev!' | |
); | |
// ... lines 15 - 22 | |
$this->assertEquals('/api/programmers/ObjectOrienter', $response->getHeader('Location')); | |
// ... lines 24 - 26 | |
} | |
// ... lines 28 - 29 |
And at the bottom, assertArrayHasKey
is good, but we really want to say assertEquals()
to really check that the nickname
key coming back is set to ObjectOrienter
:
// ... lines 1 - 24 | |
$this->assertArrayHasKey('nickname', $finishedData); | |
$this->assertEquals('ObjectOrienter', $finishedData['nickname']); | |
// ... lines 27 - 29 |
This test makes me happier. But does it pass? Run it!
php bin/phpunit -c app src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Sawheet! All green. Untilllllll you try it again:
php bin/phpunit -c app src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Now it explodes - 500 status code and we can't even see the error. But I know it's happening because nickname
is unique in the database, and now we've got the nerve to try to create a second ObjectOrienter.
Booting the Container
Ok, we've gotta take control of the stuff in our database - like by clearing everything out before each test.
If we had the EntityManager object, we could use it to help get that done. So, let's boot the framework right inside ApiTestCase
. But not to make any requests, just so we can get the container and use our services.
Symfony has a helpful way to do this - it's a base class called KernelTestCase
:
// ... lines 1 - 6 | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
class ApiTestCase extends KernelTestCase | |
{ | |
// ... lines 11 - 55 | |
} |
Inside setupBeforeClass()
, say self::bootKernel()
:
// ... lines 1 - 17 | |
public static function setUpBeforeClass() | |
{ | |
// ... lines 20 - 26 | |
self::bootKernel(); | |
} | |
// ... lines 29 - 56 |
The kernel is the heart of Symfony, and booting it basically just makes the service container available.
Add the tearDown()
method... and do nothing. What!? This is important. I'm adding a comment about why - I'll explain in a second:
// ... lines 1 - 36 | |
/** | |
* Clean up Kernel usage in this test. | |
*/ | |
protected function tearDown() | |
{ | |
// purposefully not calling parent class, which shuts down the kernel | |
} | |
// ... lines 44 - 56 |
But first, create a private function getService()
with an $id
argument. Woops - make that protected
- the whole point of this method is to let our test classes fetch services from the container. To do that, return self::$kernel->getContainer()->get($id)
:
// ... lines 1 - 50 | |
protected function getService($id) | |
{ | |
return self::$kernel->getContainer() | |
->get($id); | |
} |
The whole point of that KernelTestCase
base class is to set and boot that static $kernel
property which has the container on it. Now normally, the base class actually shuts down the kernel in tearDown()
. What I'm doing - on purpose - is booting the kernel and creating the container just once per my whole test suite.
That'll make things faster, though in theory it could cause issues or even slow things down eventually. You can experiment by shutting down your kernel in tearDown()
and booting it in setup()
if you want. Or even just clearing the EntityManager to avoid a lot of entities getting stuck inside of it after a bunch of tests.
Clearing Data
Because we have the container, we have the EntityManager. And that also means we have an easy way to clear data. Create a new private function called purgeDatabase()
. Because we have the Doctrine DataFixtures library installed, we can use a great class called ORMPurger
. Pass it the EntityManager - so $this->getService('doctrine')->getManager()
. To clear things out, say $purger->purge()
:
// ... lines 1 - 4 | |
use Doctrine\Common\DataFixtures\Purger\ORMPurger; | |
// ... lines 6 - 8 | |
class ApiTestCase extends KernelTestCase | |
{ | |
// ... lines 11 - 44 | |
private function purgeDatabase() | |
{ | |
$purger = new ORMPurger($this->getService('doctrine')->getManager()); | |
$purger->purge(); | |
} | |
// ... lines 50 - 55 | |
} |
Now we just need to call this before every test - so calling this in setup()
is the perfect spot - $this->purgeDatabase()
:
// ... lines 1 - 29 | |
protected function setUp() | |
{ | |
$this->client = self::$staticClient; | |
$this->purgeDatabase(); | |
} | |
// ... lines 36 - 56 |
This should clear the ObjectOrienter
out of the database and hopefully get things passing. Try the test!
php bin/phpunit -c app src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Drumroll! Oh no - still a 500 error. And we still can't see the error. Time to take our debugging tools up a level.
Hi,
How do I prevent an entity from being purged by ORMPurger in purgeDatabase()?
I have a couple of cases for that:
1. there are tables I don't want to clear, such as configuration tables.
2. I've got a DTO entity, that gets populated with NEW operator, which ain't got no corresponding table, but is populated by a query that joins several tables.
Thank you!