Skip to content

Test Setup

samuelgfeller edited this page Apr 22, 2024 · 3 revisions

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

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 Odan\Session\MemorySession;
use Odan\Session\SessionInterface;
use Psr\Container\ContainerInterface;
use Slim\App;
use UnexpectedValueException;

trait AppTestTrait
{
    use ContainerTestTrait;
    
    protected App $app;

    protected function setUp(): void
    {
        // Start slim app
        $this->app = require __DIR__ . '/../../config/bootstrap.php';

        // Set $this->container to container instance
        $this->setUpContainer($this->app->getContainer());

        // 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($container->get('settings')['db']['database'], 'test')) {
                throw new UnexpectedValueException('Test database name MUST contain the word "test"');
            }

            // Create tables
            $this->setUpDatabase($container->get('settings')['root_dir'] . '/resources/schema/schema.sql');

            // If fixtureTestTrait is included in the test class, insert default user roles
            if (method_exists($this, 'insertFixture')) {
                // Automatically insert user roles
                $this->insertDefaultFixtures([UserRoleFixture::class]);
            }
        }
    }

    protected function tearDown(): void
    {
        // Restore the previous error handler as PHPUnit v11 checks for any leftovers in error handlers
        restore_error_handler();

        // 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;
    // ...
}
Clone this wiki locally