Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

... 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.

Leave a comment!

9
Login or Register to join the conversation
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

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!

Reply

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!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | weaverryan | posted 5 years ago | edited

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/questions/12220198/using-doctrine-2-entity-with-different-databases with two databases, but the same thing can be set up with a single database and two entity managers.

Then in the <strong>purgeDatabase()</strong> 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 "<strong>INSERT SELECT</strong>" SQL queries:


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

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

Thank you!

Reply

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!

Reply
Default user avatar
Default user avatar Thierno Diop | posted 5 years ago

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

Reply

Hi Theirno!

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

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!

Reply
Amine Avatar

Hi,

The same issue

Cmd used : php bin/phpunit src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Reply
Default user avatar
Default user avatar dz46 | Amine | posted 5 years ago | edited

HI Amine

I was able to make it work, with these steps:

1) Create a phpunit.xml with this content:



<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <php>
        <server name="KERNEL_DIR" value="app" />
    </php>
</phpunit>

2) Run composer require indigophp/doctrine-annotation-autoload
3) Run composer dump-autoload

1 Reply

Hey Amine
Could you try running and see what happens


php vendor/bin/phpunit -c src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice