Интеграционное тестирование PHP с Docker: от базовых сценариев к промышленным решениям
Интеграционное тестирование — это критически важный этап разработки, который проверяет взаимодействие между различными компонентами системы: базой данных, кэшем, внешними API и самой бизнес-логикой. В мире PHP, где приложения часто работают в связке с MySQL/PostgreSQL, Redis и веб-серверами, Docker становится незаменимым инструментом для создания изолированных, воспроизводимых тестовых сред. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования PHP-приложений с использованием Docker, которые помогут вам вывести качество кода на новый уровень.
Почему Docker — идеальный компаньон для интеграционных тестов PHP?
Традиционные подходы к интеграционному тестированию часто страдают от проблем с окружением: базы данных могут быть разными на локальной машине и CI-сервере, порты заняты, а зависимости не совпадают. Docker решает эти проблемы радикально:
- Изоляция: каждый тестовый сценарий запускается в чистом контейнере, что исключает побочные эффекты от предыдущих тестов.
- Воспроизводимость: образы с фиксированными версиями PHP, расширений и сервисов гарантируют, что тесты пройдут одинаково на любой машине.
- Параллелизм: вы можете запускать несколько наборов тестов одновременно, каждый в своем контейнере с уникальной БД.
- Скорость: современные инструменты, такие как Testcontainers или Docker Compose, позволяют поднимать и уничтожать окружения за секунды.
Для PHP это особенно актуально, так как типичное приложение взаимодействует с множеством внешних ресурсов — от очередей (RabbitMQ) до поисковых движков (Elasticsearch).
Настройка многоуровневого тестового окружения с Docker Compose
Первый шаг к продвинутому тестированию — создание гибкого docker-compose.yml, который включает не только PHP, но и все необходимые сервисы. Рассмотрим пример для приложения на Laravel/Symfony, которое использует MySQL, Redis и внешний REST API:
version: '3.8'
services:
php-test:
image: php:8.2-cli
volumes:
- ./:/app
working_dir: /app
environment:
- DB_HOST=mysql-test
- DB_DATABASE=test_db
- REDIS_HOST=redis-test
- API_BASE_URL=http://mock-api:8080
depends_on:
mysql-test:
condition: service_healthy
redis-test:
condition: service_started
mock-api:
condition: service_started
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_db
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 5
redis-test:
image: redis:7-alpine
mock-api:
image: wiremock/wiremock:latest
volumes:
- ./tests/mock-api:/home/wiremock
Ключевой момент — использование WireMock для имитации внешних API. Это позволяет тестировать интеграцию с сервисами, которые могут быть недоступны или нестабильны в тестовой среде. WireMock запускается в отдельном контейнере и возвращает заранее заданные ответы на основе конфигурационных файлов JSON.
Для запуска тестов внутри этого окружения используйте скрипт:
#!/bin/bash
docker-compose -f docker-compose.test.yml up -d
docker-compose -f docker-compose.test.yml exec php-test vendor/bin/phpunit --testsuite=Integration
docker-compose -f docker-compose.test.yml down
Продвинутые техники: динамическое создание контейнеров и параллельный запуск
Когда проект разрастается, статический docker-compose.yml может стать узким местом. Вот где на помощь приходит библиотека Testcontainers (доступна для PHP через пакет testcontainers/testcontainers-php). Она позволяет программно создавать и уничтожать контейнеры прямо из тестового кода.
Пример теста, который динамически поднимает PostgreSQL и выполняет запросы:
use Testcontainers\Container\PostgreSQLContainer;
use PHPUnit\Framework\TestCase;
class DatabaseIntegrationTest extends TestCase
{
private static PostgreSQLContainer $postgres;
public static function setUpBeforeClass(): void
{
self::$postgres = (new PostgreSQLContainer('postgres:15'))
->withDatabaseName('integration_test')
->withUsername('test')
->withPassword('test')
->start();
}
public function testUserCanBeCreatedAndRetrieved(): void
{
$pdo = new PDO(
self::$postgres->getDsn(),
self::$postgres->getUsername(),
self::$postgres->getPassword()
);
$pdo->exec("INSERT INTO users (name, email) VALUES ('John', 'john@example.com')");
$result = $pdo->query("SELECT * FROM users WHERE email = 'john@example.com'");
$this->assertEquals('John', $result->fetch()['name']);
}
public static function tearDownAfterClass(): void
{
self::$postgres->stop();
}
}
Преимущество Testcontainers в том, что каждый тестовый класс может иметь собственное изолированное окружение. Это особенно полезно при параллельном запуске тестов — PHPUnit может выполнять несколько классов одновременно, и каждый будет работать со своим экземпляром БД, что исключает гонки данных.
Оптимизация производительности: кэширование образов и повторное использование контейнеров
На промышленных проектах время выполнения интеграционных тестов может достигать десятков минут. Вот несколько продвинутых стратегий для ускорения:
- Кэширование Docker-образов на CI: используйте GitHub Actions Cache или GitLab CI Cache для хранения слоев образов между запусками. Это сокращает время сборки с 5 минут до 30 секунд.
- Reusable containers: для тестов, которые не изменяют состояние БД (только чтение), можно создать один контейнер с предварительно заполненными данными и запускать все такие тесты против него. Это реализуется через
@dependsв PHPUnit. - Ленивое подключение: используйте пул соединений PDO или Doctrine DBAL, чтобы не создавать новое подключение для каждого теста. В комбинации с Docker это дает значительный прирост.
- Минимальный образ PHP: используйте
php:8.2-cli-alpineвместо полного образа — он в 3 раза меньше по размеру и быстрее загру