Buy

Tests with the Container

Using 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():

29 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... 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:

29 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... 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:

56 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 6
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ApiTestCase extends KernelTestCase
{
... lines 11 - 55
}

Inside setupBeforeClass(), say self::bootKernel():

56 lines src/AppBundle/Test/ApiTestCase.php
... 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:

56 lines src/AppBundle/Test/ApiTestCase.php
... 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):

56 lines src/AppBundle/Test/ApiTestCase.php
... 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():

56 lines src/AppBundle/Test/ApiTestCase.php
... 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():

56 lines src/AppBundle/Test/ApiTestCase.php
... 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.

Leave a comment!

  • 2016-07-05 weaverryan

    Ha, very clever Vlad! That makes perfect sense - but I didn't think of it :). I'm not sure if you'll run into issues if you ever need to join across entities in the 2 different entity managers - but this may also not be something you need :).

    Thanks for sharing this!

  • 2016-07-05 Vlad

    Hi Ryan,

    This can also be accomplished with two entity managers and having the corresponding entities in different subdirectories.

    This post talks about it: http://stackoverflow.com/quest... with two databases, but the same thing can be set up with a single database and two entity managers.

    Then in the purgeDatabase() method we'd have to purge using one entity manager and skip purging with the other.

    Regarding your #1 point, I also have methods that restore the original tables from backup tables using "INSERT SELECT" SQL queries:


    /** @var Connection $connection */
    $connection = $this->getEntityManager()->getConnection();

    /** @var Statement $statement */
    $statement = $connection->prepare($query);
    $statement->execute();

    Thank you!

  • 2016-06-30 weaverryan

    Yo Vlad!

    Hmm, I don't think this is possible - without basically sub-classing the ORMPurger and overriding the purge() method. But I get your use-case - it's really common for "look-up" tables - things that are effectively static, that you don't want to "clean out" every single time. Let me throw out a few options:

    1) There must be *some* way that you originally pre-populated these tables - and a new developer would need these (e.g. SQL files) to populate *their* database. You could drop *all* the data, but then execute these SQL files automatically right after the purge

    2) Override ORMPurger and avoid dropping the data in those tables. It's possible you'll run into constraint problems, but probably not (I'm guessing your configuration tables don't have foreign key columns to *other* tables that are being cleared).

    Hopefully one of these looks appetizing for you - there's unfortunately no little config option for this :)

    Cheers!

  • 2016-06-30 Vlad

    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!

  • 2016-05-22 weaverryan

    Hi Theirno!

    Hmm! That error comes from inside the KernelTestCase class: https://github.com/symfony/sym....

    Basically, it is looking for your phpunit.xml or phpunit.xml.dist file. What command are you using to execute phpunit? Do you have any non-traditional directory structure?

    Cheers!

  • 2016-05-21 Thierno Diop

    self::bootKernel() not working the error is unable to guess the kernel directory