Test Setup
Table of contents
Loading...Prerequisites
PHPUnit and test traits library
Tests in PHP are written with the PHPUnit testing framework which is the de-facto standard.
For mocking, database handling, http requests, email assertions and more, the helper traits
samuelgfeller/test-traits
are also required.
composer require --dev phpunit/phpunit
composer require --dev samuelgfeller/test-traits
Configuration
Directory structure
The tests are located in the tests
directory with the following structure:
├── tests
├── Integration # integration tests
├── Unit # unit tests
├── Fixture # database content to be added as preparation in test db for integration tests
├── Provider # data provider to run the same test cases with different data
└── Traits # utility traits (test setup, database connection, helpers)
PHPUnit configuration
The phpunit.xml
file in the root directory contains the configuration for PHPUnit.
It defines the directories that contain tests, the ones that are excluded,
and the test class suffix.
The test classes must extend the PHPUnit\Framework\TestCase
.
The APP_ENV
environment variable is set to test
.
This tells the application to use the test settings from the config/env.test.php
file.
File: phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" backupGlobals="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" cacheDirectory=".phpunit.cache"
backupStaticProperties="false">
<coverage/>
<testsuites>
<testsuite name="Integration">
<directory suffix="Test.php">tests/Integration</directory>
</testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
</testsuites>
<php>
<!-- APP_ENV has to be "test" for env.test.php (test config values) to load -->
<env name="APP_ENV" value="test"/>
<env name="PHPUNIT_TEST_SUITE" value="1"/>
</php>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>bin</directory>
<directory>build</directory>
<directory>docs</directory>
<directory>public</directory>
<directory>tmp</directory>
<directory>vendor</directory>
</exclude>
</source>
</phpunit>
Environment configuration
The environment configuration
with test-specific values is located
in config/env.test.php
:
// Enable ErrorException for notices and warnings
$settings['error']['display_error_details'] = true;
// Database for integration testing must include the word "test"
$settings['db']['database'] = 'slim_example_project_test';
// Enable test mode for the logger
$settings['logger']['test'] = true;
// ...
Test database
Before the tests that interact with the database are run,
an empty test database must be created and its name
added to the env.test.php
file with the key $settings['db']['database']
.
The tables are created during the first setUp()
call when running the test suite.
Once they're created, they are truncated for each further test.
Tables are created using the schema.sql
file which contains the code to create the tables
of the current development database.
Generating the schema file
To generate schema.sql
, the samuelgfeller/test-traits
library contains
the SqlSchemaGenerator
class.
The file bin/console.php
must be
created and the key
'SqlSchemaGenerator'
must be added to the
container definitions in the config/container.php
file.
It requires the PDO instance which can be retrieved from the CakePHP
Connection
instance.
File: config/container.php
use Psr\Container\ContainerInterface;
use Cake\Database\Connection;
return [
// ...
// CakePHP database connection
Connection::class => function (ContainerInterface $container) {
$settings = $container->get('settings')['db'];
return new Connection($settings);
},
PDO::class => function (ContainerInterface $container) {
$driver = $container->get(Connection::class)->getDriver();
$class = new ReflectionClass($driver);
$method = $class->getMethod('getPdo');
// Make function getPdo() public
$method->setAccessible(true);
return $method->invoke($driver);
},
// String key for command line call
'SqlSchemaGenerator' => function (ContainerInterface $container) {
return new \TestTraits\Console\SqlSchemaGenerator(
$container->get(PDO::class),
// Schema output folder
$container->get('settings')['root_dir'] . '/resources/schema'
);
},
];
Now as explained in Console Commands
the generateMySqlSchema()
function of the SqlSchemaGenerator
class can be called by the
command line with the help of bin/console.php
:
php bin/console.php SqlSchemaGenerator generateMySqlSchema
To make this command available as a composer script, the following can be added
to the composer.json
file script section:
"scripts": {
"schema:generate": "php bin/console.php SqlSchemaGenerator generateMySqlSchema"
}
A new schema.sql
can now be generated with the following command:
composer schema:generate
Schemas for other database types can be created,
by making a copy of the SqlSchemaGenerator
class
and adjusting the SQL queries in the function accordingly.
Setup
Test traits
Traits are a way to reuse code
easily in multiple classes.
With the use
keyword after the class definition, they can be included in a class and all
their methods are available in that class as if they'd be part of the class itself.
They are ideal for functions that are used in multiple test cases.
Test setup and teardown
Before each test function, the application must be
bootstrapped
and the database cleared if required.
This is done with the setUp()
function which is called automatically by PHPUnit before each
test function. The setup logic is the same for all tests, which means it can be extracted into a
trait. The same goes for the tearDown()
function which is called after each test function.
The AppTestTrait
located in tests/Traits
contains the setUp()
and tearDown()
functions.
The setUp()
function below also sets the
session
to a memory session and inserts
the user roles into the database by default.
File: tests/Traits/AppTestTrait.php
<?php
namespace App\Test\Trait;
use App\Test\Fixture\UserRoleFixture;
use Cake\Database\Connection;
use DI\Container;
use DI\ContainerBuilder;
use Odan\Session\MemorySession;
use Odan\Session\SessionInterface;
use Slim\App;
use TestTraits\Trait\ContainerTestTrait;
use UnexpectedValueException;
/**
* Initialize slim app for testing.
* Traits "extend" the class that include them with their content.
* Test traits: https://samuel-gfeller.ch/docs/Test-Setup#test-traits.
*/
trait AppTestTrait
{
use ContainerTestTrait;
/** @var App<\Psr\Container\ContainerInterface> */
protected App $app;
/**
* PHP Unit function setUp is called automatically before each test.
*/
protected function setUp(): void
{
// Create a new container
$container = (new ContainerBuilder())
->addDefinitions(__DIR__ . '/../../config/container.php')
->build();
// App is created in container
$this->app = $container->get(App::class);
// Set $this->container to the container instance
$this->setUpContainer($container);
// Set memory sessions
$this->setContainerValue(SessionInterface::class, new MemorySession());
// If setUp() is called in a testClass that uses DatabaseTestTrait, the method setUpDatabase() exists
if (method_exists($this, 'setUpDatabase')) {
// Check that database name from config contains the word "test"
// This is a double security check to prevent unwanted use of dev db for testing
if (!str_contains($this->container->get('settings')['db']['database'], 'test')) {
throw new UnexpectedValueException('Test database name MUST contain the word "test"');
}
// Create tables
$this->setUpDatabase($this->container->get('settings')['root_dir'] . '/resources/schema/schema.sql');
// If DatabaseTestTrait is included in the test class (function below exits), insert default user roles
if (method_exists($this, 'insertDefaultFixtureRecords')) {
// Automatically insert user roles
$this->insertDefaultFixtureRecords([UserRoleFixture::class]);
}
}
}
/**
* Teardown function is called automatically after each test.
*/
protected function tearDown(): void
{
// Disconnect from database to avoid "too many connections" errors
if (method_exists($this, 'setUpDatabase')) {
$connection = $this->container->get(Connection::class);
$connection->rollback();
$connection->getDriver()->disconnect();
if ($this->container instanceof Container) {
$this->container->set(Connection::class, null);
$this->container->set(\PDO::class, null);
}
}
}
}
In each test class, the AppTestTrait
is included with the use
keyword
at the top of the class.
namespace App\Test\Integration;
use PHPUnit\Framework\TestCase;
use App\Test\Trait\AppTestTrait;
class ExampleTest extends TestCase
{
use AppTestTrait;
// ...
}
Running tests
Most IDEs have built-in support for running tests, but they can also be run from the command line.
The following command shortcut is defined in the
scripts
section
of the composer.json
file:
composer test
Continuous Integration
To run the tests automatically when pushing, GitHub Actions can be used in combination with tools like Scrutinizer or Codecov to get insights into the code quality.
For more information, see GitHub Actions.