Symfony PHPUnit tests using Doctrine
18 Jun 2017You can use Symfony features to easily create our test case. It automatically keeps your database schema up-to-date, creates fixtures and removes data after a test.
The basic use of PHPUnit with Symfony is well documented in the Official Symfony documentation, but what about tests using a relational database with Doctrine? It doesn’t work “out of the box” in Symfony. So we have to write it ourselves. Here you can read about different approaches for maintaining a database with tests. In this article, I’ll show you how to implement the “Update when needed” process with fixtures.
Keeping database up-to-date
Nobody wants to think about maintaining the test database. Just create a schema once and set up the credentials in the configuration file(config/test/config.yml
). Then don’t worry about updating it - it will be done automatically.
There is great functionality to do it hassle-free - using Doctrine’s SchemaTool::updateSchema()
.
The main downside is that it can take some time, depending on the size of database schema (from 1 second for a small/medium schema to several seconds on a big schema!).
I created a separate class because it’s needed to extend the KernelTestCase
to get the application’s kernel
.
class GenerateSchema extends KernelTestCase
{
public function generate()
{
$kernel = static::createKernel();
$kernel->boot();
$entityManager = $kernel->getContainer()->get('doctrine.orm.entity_manager');
$metadata = $entityManager->getMetadataFactory()->getAllMetadata();
$tool = new SchemaTool($entityManager);
$tool->updateSchema($metadata);
}
}
You can use the above class in function DbTestCase::setUpBeforeClass()
, but it would execute before every test class, which is a complete runtime time waste. It’s better to execute it once.
It doesn’t matter if we run all tests, a specific test suite or a single test. It will be always executed once. To do that you will need to create a custom bootstrap.php
to add these functionalities, e.g. tests.bootstrap.php
.
<?php
require __DIR__ . '/bootstrap.php.cache';
$schema = new \Tests\GenerateSchema();
$schema->generate();
Then set the new bootstrap
in the phpunit configuration file (default: app/phpunit.xml.dist
)
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="tests.bootstrap.php"
>
...
</phpunit>
Remember to execute phpunit using this configuration, e.g.
phpunit -c app/phpunit.xml.dist tests/
Fixtures
To have the possibility if adding specific fixtures for a test you can use Symfony’s fixture functionality.
There are two methods you can use: DbTestCase::addFixture()
and DbTestCase::execute()
which can be used by chaining, e.g.
$this->addFixture(new UsersFixture())->executeFixtures();
I extended WebTestCase
because I would use it to test HTTP responses. If you don’t need it, just extend KernelTestCase
. Then you will need to extend this test case in your every test class.
Also, I exposed EntityManager
for use in tests, to get the necessary repositories from entity class.
abstract class DbTestCase extends WebTestCase
{
/**
* @var Loader
*/
private $loader;
/**
* @var EntityManager
*/
public static $em;
protected function setUp()
{
parent::setUp();
self::bootKernel();
self::$em = self::$kernel->getContainer()->get('doctrine.orm.entity_manager');
}
/**
* Adds a new fixture to be loaded.
*
* @param FixtureInterface $fixture
* @return $this
*/
protected function addFixture(FixtureInterface $fixture)
{
if (!$this->loader) {
$this->loader = new Loader();
}
$this->loader->addFixture($fixture);
return $this;
}
/**
* Executes all the fixtures that have been loaded so far.
*/
protected function executeFixtures()
{
$purger = new ORMPurger();
$executor = new ORMExecutor(self::$em, $purger);
$executor->execute($this->loader->getFixtures(), true); //append fixtures intead of cleaning
}
}
Cleaning the database
If you have fixtures for every table you set ORMExecutor
to remove data from a database for tables with fixtures.
However, it’s not a bulletproof solution. Imagine if a new table is created without a fixture. Then data in this table will be not cleaned after each test execution.
There is a fast and efficient solution - wrapping every single test into a database transaction. To do it we can use this library for Symfony (doctrine-test-bundle)
Remember to clean the whole schema used for tests before using it again.