Интеграционное тестирование PHP с Docker: от А до Я
Современная PHP-разработка немыслима без качественного тестирования. Если модульные тесты проверяют изолированные куски кода, то интеграционное тестирование убеждается, что все компоненты системы работают вместе: база данных, Redis, внешние API и файловая система. Docker делает этот процесс воспроизводимым, быстрым и независимым от окружения разработчика. В этой статье мы разберем, как настроить интеграционное тестирование PHP-приложения с помощью Docker с нуля.
Почему Docker — идеальный инструмент для интеграционных тестов?
Традиционные проблемы интеграционного тестирования:
- Зависимость от окружения — у каждого разработчика своя версия MySQL, PHP, Redis.
- Загрязнение данных — тесты оставляют мусор в реальной БД.
- Сложность параллельного запуска — тесты конфликтуют друг с другом.
Docker решает эти проблемы:
- Каждый тест запускается в чистом контейнере с известными версиями зависимостей.
- Контейнеры изолированы и удаляются после прогона.
- Docker Compose позволяет поднять всю инфраструктуру одной командой.
Шаг 1: Структура проекта и Docker Compose
Начнем с типичного PHP-приложения на Symfony. Создадим файл docker-compose.test.yml для тестового окружения:
version: '3.8'
services:
php:
build:
context: .
dockerfile: Dockerfile.test
environment:
DATABASE_URL: "mysql://test:test@mysql:3306/test_db?serverVersion=8.0"
REDIS_URL: "redis://redis:6379"
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_db
MYSQL_USER: test
MYSQL_PASSWORD: test
tmpfs: /var/lib/mysql # Данные в памяти — быстрее
redis:
image: redis:7-alpine
tmpfs: /data
Файл Dockerfile.test:
FROM php:8.2-cli
RUN docker-php-ext-install pdo_mysql
RUN pecl install redis && docker-php-ext-enable redis
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install --no-interaction --prefer-dist
CMD ["vendor/bin/phpunit", "--configuration", "phpunit.xml.dist"]
Обратите внимание на tmpfs — это монтирование временной файловой системы в память. База данных и Redis будут работать в разы быстрее, чем на диске, что критично для быстрых тестов.
Шаг 2: Настройка PHPUnit для интеграционных тестов
Создадим отдельную конфигурацию phpunit.integration.xml.dist:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true">
<testsuites>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="test"/>
<env name="KERNEL_CLASS" value="App\Kernel"/>
</php>
</phpunit>
Файл tests/bootstrap.php для инициализации тестового окружения:
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
// Ждем, пока MySQL и Redis будут готовы
$maxAttempts = 30;
$connected = false;
for ($i = 0; $i < $maxAttempts; $i++) {
try {
$pdo = new PDO(
$_ENV['DATABASE_URL'],
null, null,
[PDO::ATTR_TIMEOUT => 1]
);
$connected = true;
break;
} catch (PDOException $e) {
sleep(1);
}
}
if (!$connected) {
throw new RuntimeException('Не удалось подключиться к MySQL');
}
// Создаем схему БД
$pdo->exec(file_get_contents(dirname(__DIR__) . '/migrations/schema.sql'));
Шаг 3: Пишем интеграционные тесты
Рассмотрим три типичных сценария: работа с БД, Redis и внешним API.
Тестирование базы данных
<?php
namespace App\Tests\Integration;
use PHPUnit\Framework\TestCase;
use App\Repository\UserRepository;
class UserRepositoryTest extends TestCase
{
private static \PDO $pdo;
public static function setUpBeforeClass(): void
{
self::$pdo = new PDO($_ENV['DATABASE_URL']);
// Очищаем таблицу перед тестом
self::$pdo->exec('TRUNCATE TABLE users');
}
public function testCreateUser(): void
{
$repo = new UserRepository(self::$pdo);
$userId = $repo->create('test@example.com', 'password_hash');
$this->assertIsInt($userId);
$this->assertGreaterThan(0, $userId);
// Проверяем, что пользователь действительно сохранен
$user = $repo->findById($userId);
$this->assertNotNull($user);
$this->assertEquals('test@example.com', $user['email']);
}
}
Тестирование работы с Redis
<?php
namespace App\Tests\Integration;
use PHPUnit\Framework\TestCase;
use App\Service\SessionManager;
class SessionManagerTest extends TestCase
{
private static \Redis $redis;
public static function setUpBeforeClass(): void
{
self::$redis = new \Redis();
self::$redis->connect('redis', 6379);
self::$redis->flushAll(); // Очищаем перед тестом
}
public function testSetAndGetSession(): void
{
$manager = new SessionManager(self::$redis);
$sessionId = 'test_session_123';
$data = ['user_id' => 1, 'role' => 'admin'];
$manager->set($sessionId, $data, 3600);
$result = $manager->get($sessionId);
$this->assertEquals($data, $result);
}
public