Clearing the Database
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 SubscribeEach time we run the test, it adds more and more entries to the database. Our test is not reliable: it depends on what the database looks like when it starts. The solution is simple: we must control the data in the database at the start of every test.
Creating a Test Database
Step one to accomplishing this is to use a different database for our test environment. Actually, this is mostly for convenience: using the same database for testing and development is annoying. One minute, you're coding through something awesome in your browser, then you run your tests, then back in the browser, all your nice data is gone or totally different! Isolation in this instance is nice.
To do that, go back to config_test.yml
. In a Symfony 4 Flex application, you should create a config/packages/test/doctrine.yaml
file since this will contain Doctrine configuration that you only want to use in the test
environment.
Inside, anywhere, add doctrine
, dbal
then url
set to sqlite:///%kernel.project_dir%/var/data/test.sqlite
.
// ... lines 1 - 23 | |
doctrine: | |
dbal: | |
url: 'sqlite:///%kernel.project_dir%/var/data/test.sqlite' | |
// ... lines 27 - 28 |
This will override the settings in app/config/config.yml
- the stuff under doctrine
. With the new config, we're completely replacing this stuff and saying "Hey! Use an sqlite, flat-file database!".
Why Sqlite? It's simple to setup and you can use some tricks to speed up your tests. We'll see that in a few minutes.
Oh, and make sure you have the %
sign at the end of kernel.project_dir
!
Now, find your terminal and create the var/data
directory:
mkdir var/data
Next, create the schema
php bin/console doctrine:schema:create --env=test
And, congrats! You are the owner of a fancy new var/data/test.sqlite
file! Take good care of it.
Clearing the Database before Tests
At this point, not much has changed really. Our tests will still pass one time, but will fail each time after. We haven't actually fixed the problem yet!
How can we? The best way is to fully empty the database at the beginning of each test. This would certainly put our database into a known state: empty! Then, if we do need any data before running the test, we can manually add it in the test. It's not super fancy, but it keeps everything really clear.
Like most good things in life, there are two ways to do this. First, if you downloaded the course code, then in the tutorial/tests
directory, you'll find an EnclosureBuilderServiceIntegrationTest.php
file. Copy the truncateEntities()
method and paste that into your test class.
// ... lines 1 - 11 | |
class EnclosureBuilderServiceIntegrationTest extends KernelTestCase | |
{ | |
// ... lines 14 - 50 | |
private function truncateEntities(array $entities) | |
{ | |
$connection = $this->getEntityManager()->getConnection(); | |
$databasePlatform = $connection->getDatabasePlatform(); | |
if ($databasePlatform->supportsForeignKeyConstraints()) { | |
$connection->query('SET FOREIGN_KEY_CHECKS=0'); | |
} | |
foreach ($entities as $entity) { | |
$query = $databasePlatform->getTruncateTableSQL( | |
$this->getEntityManager()->getClassMetadata($entity)->getTableName() | |
); | |
$connection->executeUpdate($query); | |
} | |
if ($databasePlatform->supportsForeignKeyConstraints()) { | |
$connection->query('SET FOREIGN_KEY_CHECKS=1'); | |
} | |
} | |
// ... lines 72 - 81 | |
} |
This is simple: pass the method an array of entities and it will empty them.
You might want to call this at the top of every test method. But another great option is to override setUp()
and add it there. Let's empty all three entities: Enclosure
, Security
and Dinosaur
.
// ... lines 1 - 11 | |
class EnclosureBuilderServiceIntegrationTest extends KernelTestCase | |
{ | |
public function setUp() | |
{ | |
self::bootKernel(); | |
$this->truncateEntities([ | |
Enclosure::class, | |
Security::class, | |
Dinosaur::class, | |
]); | |
} | |
// ... lines 24 - 81 | |
} |
For this method to work, we need a getEntityManager()
method. At the bottom, add private function getEntityManager()
. Then, copy our logic from above, paste it here, and add return
. And since you know I love auto-completion, I'll add a @return EntityManager
.
// ... lines 1 - 72 | |
/** | |
* @return EntityManager | |
*/ | |
private function getEntityManager() | |
{ | |
return self::$kernel->getContainer() | |
->get('doctrine') | |
->getManager(); | |
} | |
// ... lines 82 - 83 |
This makes truncateEntities()
happy! And we can even use getEntityManager()
above.
// ... lines 1 - 24 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 27 - 31 | |
$em = $this->getEntityManager(); | |
// ... lines 33 - 48 | |
} | |
// ... lines 50 - 83 |
Oh, and it's really important that we call self::bootKernel()
before we try to access any services. The best thing to do is remove it from the test method and add it to setUp()
.
// ... lines 1 - 13 | |
public function setUp() | |
{ | |
self::bootKernel(); | |
// ... lines 17 - 22 | |
} | |
// ... lines 24 - 83 |
Done! Try the tests:
./vendor/bin/phpunit --filter testItBuildsEnclosureWithTheDefaultSpecification
We got it! We can run them over, and over, and over again. Always green!
Using Data Fixtures
This was the more manual way to clear the database, and it gives you a bit more control. Another option is to use Doctrine's DataFixtures library.
First, install it:
composer require "doctrine/data-fixtures:^1.3" --dev
When this finishes, we can delete all the logic in truncateEntities()
... because now we have a fancy "purger" object: $purger = new ORMPurger()
and pass in the entity manager.
Then... $purger->purger()
. And yea... that's it! We can remove the $entities
argument and stop passing in the array.
// ... lines 1 - 47 | |
private function truncateEntities() | |
{ | |
$purger = new ORMPurger($this->getEntityManager()); | |
$purger->purge(); | |
} | |
// ... lines 53 - 64 |
This loops over all of your entity objects and deletes them one by one. It will even delete them in the correct order to avoid foreign key problems. But, if you have two entities that both have foreign keys pointing at each other, you may still have problems.
But, this works! The tests still pass.
Before we move on, I want to show you one cool, weird trick with integration tests: I call it "partial" mocking.
Somehow for me the url key was not working, as I got the following errors:
[Doctrine\ORM\Tools\ToolsException]
Schema-Tool failed with Error 'An exception occurred in driver: SQLSTATE[HY000] [14] unable to open database file' while executing DDL: CREATE TABLE dinosaurs (id INTEGER NOT NULL, encl
osure_id INTEGER DEFAULT NULL, length INTEGER NOT NULL, genus VARCHAR(255) NOT NULL, is_carnivorous BOOLEAN NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_B1DE6E91D04FE1E5 FOREIGN KEY (enclos
ure_id) REFERENCES enclosure (id) NOT DEFERRABLE INITIALLY IMMEDIATE)
[Doctrine\DBAL\Exception\ConnectionException]
An exception occurred in driver: SQLSTATE[HY000] [14] unable to open database file
[Doctrine\DBAL\Driver\PDOException]
SQLSTATE[HY000] [14] unable to open database file
[PDOException]
SQLSTATE[HY000] [14] unable to open database file
In the end I resolved it by replacing the url key by path, and removing the sqlite:// part. As in:
That did the trick for me.