Тесты
Тест — код, который сам проверяет приложение: отправляет запрос и сравнивает ответ с ожидаемым. Приложение Slim — объект, который принимает запрос и возвращает ответ, поэтому веб-сервер для тестов не нужен: запрос создают прямо в коде. Здесь и пригодится файл config/bootstrap.php, который возвращает приложение, не запуская его.
Установка
composer require --dev phpunit/phpunitКлюч --dev помечает пакет как нужный только при разработке.
Где лежат тесты
Создайте в корне проекта папку tests и файл настроек phpunit.xml:
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="app">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>Файл сообщает PHPUnit, где искать тесты. Имена файлов с тестами заканчиваются на Test.php.
Сессия в консоли
Тесты запускаются из консоли, а там session_start() выдаёт предупреждение. Поэтому сначала поправьте условие в SessionMiddleware:
// src/Middleware/SessionMiddleware.php
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {PHP_SAPI равен cli, когда PHP запущен из консоли. В тестах страниц сайта перед запросом задают $_SESSION = []; — посредники, читающие сессию, получат пустой массив.
Первый тест
// 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 создаёт запрос с нужным методом и адресом. Метод handle() проводит его через все посредники и маршруты и возвращает ответ, ничего не выводя. Запуск:
vendor/bin/phpunitКоманда напечатает OK (1 test, 1 assertion).
База для тестов
Тест не должен трогать настоящую базу. До первого запроса запись PDO::class в контейнере заменяют базой SQLite в памяти — она создаётся пустой и исчезает после теста. Добавьте в класс теста метод:
// 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 ('Тестовая статья', 'Текст')");
$app->getContainer()->set(PDO::class, $pdo);
return $app;
}Метод set() контейнера PHP-DI подменяет запись. Репозиторий получит поддельное подключение и не заметит разницы.
Проверка ответа JSON
// 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('Тестовая статья', $data['data'][0]['title']);
}Тело ответа — поток, поэтому перед разбором его приводят к строке.
Запрос с данными
// tests/ApiPostsTest.php
public function testCreateRequiresToken(): void
{
$request = (new ServerRequestFactory())
->createServerRequest('POST', '/api/posts')
->withParsedBody(['title' => 'Новая статья', 'body' => 'Текст']);
$this->assertSame(401, $this->createApp()->handle($request)->getStatusCode());
}Заголовки добавляют методом withHeader(), данные — withParsedBody(). Без токена посредник отвечает кодом 401, и статья не создаётся.
Частая ошибка
Тест, который работает с настоящей базой, зависит от её содержимого: сегодня проходит, а после удаления статьи падает, да ещё и оставляет после себя мусор. Каждый тест должен сам создать нужные ему данные и не зависеть от других тестов и порядка их запуска.