PHPUnit tests are used to test isolated pieces of code in your application. At DeltaBlue we did some brainstorming on this subject and came to the following conclusions:
- As we have a gigantic codebase, including microservices, it’s impractical to test single PHP classes.
- Therefore we better focus on Isolated Action testing, meaning we will test the action calls.
- Focussing on action calls, we can organise the tests by Controller.
To setup PHPUnit testing, we alter phpunit.xml.dist to the following:
<?xml version="1.0" encoding="UTF-8"?>
<!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
stopOnFailure="true"
bootstrap="tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_DIR" value="app/" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
<env name="SYMFONY_ENV" value="test" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
<exclude>
<directory>src/*Bundle/Resources</directory>
<directory>src/*/*Bundle/Resources</directory>
<directory>src/*/Bundle/*Bundle/Resources</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
What’s to be noticed here? Tests will be stored in the tests directory ( under root ), the SYMFONY_ENV parameter is set to test, which will call config_test.yml when bootstrapping AND we will use a custom bootstrap for testing.
That custom bootstrap looks like :
<?php
passthru('php '. __DIR__. '/../bin/console ca:cl');
passthru('php '. __DIR__. '/../bin/console doc:database:create');
passthru('php '. __DIR__. '/../bin/console doc:sch:up -f');
passthru('php '. __DIR__. '/../bin/console doctrine:fixtures:load -n', $result);
if ( $result != 0) {
exit("Could not fully execute fixtures.\n");
}
require __DIR__.'/../app/autoload.php';
Before every test run, we will create a new database and load fixtures, so our tests always run on the same database. Best way to do this, is to setup sqlite for your testing.
Setup of sqlite is simply done in config_test.yml
imports:
- { resource: config_dev.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
doctrine:
dbal:
default_connection: default
connections:
default:
driver: pdo_sqlite
path: "%kernel.cache_dir%/test.db"
services:
some_awesome_service:
class: TestBundle\Services\AwesomeService
This will create a little test.db file on each test run, which will contain your database. Tests will always run on the same dataset. This has some pros and cons, which you should discuss internally to decide if this is the best approach for your situation or not.
config_test.yml can also be used to override some services to use a mocking service during testing. This is awesome, because this means you can fully isolate your tests and e.g. replace a service that makes an external API call so it always returns the same output!
Now all that’s left to do is write your tests. If you have e.g. an action that lists all projects, and that action is the ListAction method in the MYBUNDLE/ProjectController, the corresponding test would be:
tests/MYBUNDLE/Controller/Project/ListActionTest
And could look something like:
<?php
namespace Tests\MYBUNDLE\Controller\Project;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ListActionTest extends WebTestCase
{
public function getRoute()
{
return 'application.project.list';
}
public function testSuccesfull()
{
$client = $this->createClient(['test_case' => 'authorizedClient', 'root_config' => 'config.yml']);
$client->request( 'GET', $this->getPath());
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
return $response;
}
public function getPath()
{
$router = static::$kernel->getContainer()->get('router');
return $router->generate($this->getRoute());
}
}
So for every one of your Actions, add a unit test to check a successful call, an unauthorized call,… until every call is covered.