3 minutes
How to write unit tests with PHPUnit involving Doctrine entities
One of the main reasons for me why tight coupling is a less ideal design choice is the accompanied difficulty in testing these tight coupled components.
Today I was asked to assist in refactoring a component which computes data retrieved from nearly all entities. There were no tests for the units in question. To ensure that the code would end with the expected result after refactoring, tests were needed. But how do you test such code? In a perfect world scenario, each unit would be so small and decoupled that writing a test would be simple, not having to deal with the variance of a changing dataset. After some contemplation, I decided to start by writing tests with a defined dataset for the tests, making it possible to rely on test data.
Disclaimer: The solution presented in this article is to be considered as working solution for the described issue and nothing more. It is neither a general right fit for testing units relying on database interactions nor best practice.
Test data
For the tests, that rely on database interactions - in this article read-only - I provide a defined dataset as a MySQL dump. For generating a dump, please refer to the documentation. Ensure that either the database or tables are dropped before creation, so that each time the dump is imported, a fresh clean database schema and data is provided. I zipped the dump and stored it in a place for test assets, in my case test\TestAsset\Database\testing.sql.7z
.
Installing test data before each test
With the help of the setUp()
method, the dump is imported before each test. To safe on disk space, the dump is archived. On Windows my favorite choice is 7-Zip because of better compression rates and sometime speed compared to other formats. Extraction requires the 7-Zip binary. To import the dump, the mysql.exe
, a database username and password as well as a database is required. The latter might be irrelevant if you set it up in the dump.
This setup works on a Windows environment only and would need further changes to make automated testing including other environments possible.
To configure the options, the PHPUnit configuration file put to work.
phpunit.xml.dist
<php>
<env name="SEVEN_ZIP_EXE" value="C:/Program Files/7-Zip/7z.exe" force="true" />
<env name="MYSQL_EXE" value="C:/Program Files/MySQL/MySQL Server 8.0/bin/mysql.exe" force="true" />
<env name="DATABASE_USERNAME" value="acme_unit_test" force="true" />
<env name="DATABASE_PASSWORD" value="abcdefgh01234567" force="true" />
<env name="DATABASE" value="acme_unit_test" force="true" />
</php>
setUp()
Now to extract and import the dump, the setUp()
method is put to work.
<?php
declare(strict_types = 1);
namespace AcmeTest;
use PHPUnit\Framework\TestCase;
use Acme\RoadRunner;
class RoadRunnerTest extends TestCase
{
protected function setUp() : void
{
$zippedFile = __DIR__ . '/../../TestAsset/Database/testing.sql.7z';
$dumpFile = __DIR__ . '/../../TestAsset/Database/testing.sql';
$extractPath = __DIR__ . '/../../TestAsset/Database';
$extractCommand = sprintf(
'"%s" x %s -o%s -y',
getenv('SEVEN_ZIP_EXE'),
$zippedFile,
$extractPath
);
exec($extractCommand);
$importCommand = sprintf(
'cmd /c "%s" -u %s -p%s %s < %s',
getenv('MYSQL_EXE'),
getenv('DATABASE_USERNAME'),
getenv('DATABASE_PASSWORD'),
getenv('DATABASE'),
$dumpFile
);
exec($importCommand);
}
// Some tests
}
Write tests
Finally, we can start writing tests.
<?php
declare(strict_types = 1);
namespace AcmeTest;
use PHPUnit\Framework\TestCase;
use Acme\RoadRunner;
class RoadRunnerTest extends TestCase
{
// setUp()...
public function testGetMessagesInitialValue() : void
{
// RoadRunner relying on data provided in setUp()
$roadRunner = new RoadRunner();
$this->assertSame($expected, $roadRunner->meepMeep());
}
}
Conclusion
The component in question can now be tested and refactored. Results before and after refactoring can be validated. The goal is achieved.
However, it comes with a price. This setup conflicts two of the FIRST (Fast, Isolated, Repeatable, Self-Validating, Timely) properties of unit tests.
The tests are not
- fast: Database import is costly, execution time is slowed down noticeably,
- isolated: They rely on external data (database access).