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.

^