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.
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.
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.
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!
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!
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!
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!
Hi,
The same issue
Cmd used : php bin/phpunit src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
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
Hey Amine
Could you try running and see what happens
php vendor/bin/phpunit -c src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Cheers!
// 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
}
}
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!