Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Testing

A test is code that checks the application by itself: it sends a request and compares the response with the expected one. A Slim application is an object that takes a request and returns a response, so tests need no web server: the request is created right in code. This is where config/bootstrap.php, which returns the application without running it, comes in useful.

Installation

composer require --dev phpunit/phpunit

The --dev option marks the package as needed only during development.

Where tests live

Create a tests folder in the project root and a phpunit.xml settings file:

<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="app">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

The file tells PHPUnit where to look for tests. Test file names end with Test.php.

The session in the console

Tests run from the console, where session_start() raises a warning. So first adjust the condition in SessionMiddleware:

// src/Middleware/SessionMiddleware.php
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {

PHP_SAPI equals cli when PHP is started from the console. In tests of site pages, $_SESSION = []; is set before the request — middleware that reads the session gets an empty array.

The first test

// tests/ApiPostsTest.php
use PHPUnit\Framework\TestCase;
use Slim\Psr7\Factory\ServerRequestFactory;
 
final class ApiPostsTest extends TestCase
{
    public function testUnknownAddressReturns404(): void
    {
        $app = require __DIR__ . '/../config/bootstrap.php';
 
        $request = (new ServerRequestFactory())->createServerRequest('GET', '/api/nothing');
        $response = $app->handle($request);
 
        $this->assertSame(404, $response->getStatusCode());
    }
}

ServerRequestFactory creates a request with the given method and address. The handle() method takes it through all middleware and routes and returns the response without printing anything. Run it:

vendor/bin/phpunit

The command prints OK (1 test, 1 assertion).

A database for tests

A test must not touch the real database. Before the first request the PDO::class entry in the container is replaced with an in-memory SQLite database — it starts empty and disappears after the test. Add a method to the test class:

// tests/ApiPostsTest.php
private function createApp(): \Slim\App
{
    $app = require __DIR__ . '/../config/bootstrap.php';
 
    $pdo = new PDO('sqlite::memory:', null, null, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    $pdo->exec('CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT, created_at TEXT)');
    $pdo->exec("INSERT INTO posts (title, body) VALUES ('Test post', 'Text')");
 
    $app->getContainer()->set(PDO::class, $pdo);
 
    return $app;
}

The set() method of the PHP-DI container replaces an entry. The repository receives the substitute connection and notices no difference.

Checking a JSON response

// tests/ApiPostsTest.php
public function testListReturnsPosts(): void
{
    $request = (new ServerRequestFactory())->createServerRequest('GET', '/api/posts');
    $response = $this->createApp()->handle($request);
 
    $data = json_decode((string) $response->getBody(), true);
 
    $this->assertSame(200, $response->getStatusCode());
    $this->assertSame('Test post', $data['data'][0]['title']);
}

The response body is a stream, so it is cast to a string before parsing.

A request with data

// tests/ApiPostsTest.php
public function testCreateRequiresToken(): void
{
    $request = (new ServerRequestFactory())
        ->createServerRequest('POST', '/api/posts')
        ->withParsedBody(['title' => 'New post', 'body' => 'Text']);
 
    $this->assertSame(401, $this->createApp()->handle($request)->getStatusCode());
}

Headers are added with withHeader(), data with withParsedBody(). Without a token the middleware answers with code 401, and no post is created.

Common mistake

A test that works with the real database depends on its contents: today it passes, after a post is deleted it fails, and it leaves rubbish behind as well. Each test must create the data it needs by itself and must not depend on other tests or on the order they run in.

Thanks for reading!

Donate to the site — we work hard for you.