Example below helps us when we deal with dynamic response content while testing Symfony controllers with PHPUnit. In this example the id and created_at fields are dynamic so each response would have different values. The reason why the data is always dynamic is because they are loaded by Doctrine DataFixtures.


Flow


  1. The setUp method reloads data fixtures before running each test cases.

  2. The replaceDynamicData method replaces any dynamic data with a static one for given rules.

  3. The tearDownAfterClass method reloads data fixtures when whole test suite ends.

Configuration


These configurations are for isolating test, dev and prod environments so each environment will have their own databases such as api_test, api_dev and api.


.env.dist


APP_ENV=dev
DATABASE_URL=mysql://root:root@127.0.0.1:3306/api

config/packages/doctrine.yaml


doctrine:
dbal:
...
url: '%env(resolve:DATABASE_URL)%'

config/packages/test/doctrine.yaml


doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%_%kernel.environment%'

config/packages/dev/doctrine.yaml


doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%_%kernel.environment%'

phpunit.xml.dist





xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php">













tests





src




Installation


Make sure you have phpunit/phpunit and symfony/browser-kit composer packages installed.


Test files


AbstractTest


declare(strict_types=1);

namespace App\Tests;

use App\DataFixtures\CountryFixtures;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Loader;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class AbstractTest extends WebTestCase
{
/** @var Client $client */
protected $client;

public static function tearDownAfterClass()
{
self::reloadDataFixtures();
}

protected function setUp()
{
self::reloadDataFixtures();

$this->client = static::createClient();
}

protected function replaceDynamicData(string $data): string
{
return preg_replace(
[
'/"id":\d/',
'/\b(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\+(\d{4})\b/',
],
[
'"id":AUTO_ID',
'DATE_ISO8601',
],
$data
);
}

private static function reloadDataFixtures(): void
{
$kernel = static::createKernel();
$kernel->boot();
$entityManager = $kernel->getContainer()->get('doctrine')->getManager();

$loader = new Loader();
foreach (self::getFixtures() as $fixture) {
$loader->addFixture($fixture);
}

$purger = new ORMPurger();
$purger->setPurgeMode(ORMPurger::PURGE_MODE_DELETE);
$executor = new ORMExecutor($entityManager, $purger);
$executor->execute($loader->getFixtures());
}

private static function getFixtures(): iterable
{
return [
new CountryFixtures(),
];
}
}

CountryControllerTest


declare(strict_types=1);

namespace App\Tests\Controller;

use App\Tests\AbstractTest;

class CountryControllerTest extends AbstractTest
{
/**
* @test
* @dataProvider getAllDataProvider
*/
public function get_all_returns_valid_response(int $code, string $body)
{
$this->client->request('GET', '/api/v1/countries');

$this->assertSame($code, $this->client->getResponse()->getStatusCode());
$this->assertSame(
$this->replaceDynamicData(json_encode(json_decode($body, true))),
$this->replaceDynamicData($this->client->getResponse()->getContent())
);
}

public function getAllDataProvider(): iterable
{
return [
[
'$code' => 200,
'$body' => <<[
{
"id": 1,
"code": "gb",
"name": "Great Britain",
"createdAt": "2000-10-29T21:57:00+0100"
},
{
"id": 2,
"code": "tr",
"name": "Turkey",
"createdAt": "2001-11-30T22:58:01+0200"
},
{
"id": 3,
"code": "de",
"name": "Germany",
"createdAt": "2002-12-31T23:59:02+0300"
}
]
EOT
]
];
}
}

Test


$ vendor/bin/phpunit --filter CountryControllerTest tests/Controller/CountryControllerTest.php 
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 1.77 seconds, Memory: 22.00MB

OK (1 test, 2 assertions)