Интеграционное тестирование PHP с Docker: продвинутый уровень
Интеграционное тестирование — критически важный этап разработки, особенно для сложных PHP-приложений, взаимодействующих с базами данных, очередями, внешними API и другими сервисами. Docker позволяет поднять изолированное окружение, максимально приближенное к production, и выполнять тесты в воспроизводимых условиях. В этой статье мы разберём продвинутые техники: организацию тестовых контейнеров, симуляцию внешних зависимостей, параллельный запуск и интеграцию с CI/CD.
1. Архитектура тестового окружения с docker-compose
Для интеграционных тестов мы используем отдельный docker-compose.test.yml, который поднимает только необходимые сервисы: PHP-FPM с тестовым кодом, базу данных (например, PostgreSQL), Redis и, опционально, mock-сервер для внешних API.
version: '3.8'
services:
php-test:
build:
context: .
dockerfile: Dockerfile.test
volumes:
- .:/var/www/html
environment:
DATABASE_URL: "pgsql://test:test@db-test:5432/testdb"
REDIS_URL: "redis://redis-test:6379"
EXTERNAL_API_URL: "http://mock-server:1080"
depends_on:
- db-test
- redis-test
- mock-server
db-test:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5433:5432" # Избегаем конфликта с основной БД
redis-test:
image: redis:7-alpine
ports:
- "6380:6379"
mock-server:
image: mockserver/mockserver:latest
environment:
MOCKSERVER_PROPERTY_FILE: /config/mockserver.properties
volumes:
- ./tests/mockserver:/config
ports:
- "1080:1080"
Обратите внимание: мы используем отдельные порты для тестовых сервисов, чтобы они не мешали локальной разработке.
2. Подготовка PHPUnit и инициализация тестовой БД
Настройка phpunit.xml для интеграционных тестов:
<phpunit bootstrap="tests/bootstrap.php">
<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 отвечает за инициализацию схемы БД и заполнение тестовыми данными:
require __DIR__ . '/../vendor/autoload.php';
// Создаём схему БД
$pdo = new PDO('pgsql:host=db-test;port=5432;dbname=testdb', 'test', 'test');
$pdo->exec(file_get_contents(__DIR__ . '/../migrations/schema.sql'));
// Загружаем фикстуры
require __DIR__ . '/Fixtures/LoadFixtures.php';
Такой подход гарантирует, что каждый запуск тестов начинается с чистого, предсказуемого состояния.
3. Тестирование с внешними API: MockServer и WireMock
Для имитации внешних HTTP-сервисов используем MockServer (или WireMock). Создаём конфигурацию ожиданий в JSON-файле:
// tests/mockserver/expectations/payment.json
{
"httpRequest": {
"method": "POST",
"path": "/api/v1/charge"
},
"httpResponse": {
"statusCode": 200,
"body": "{\"status\": \"success\", \"transaction_id\": \"txn_123\"}",
"headers": {
"Content-Type": "application/json"
}
}
}
В PHP-коде теста отправляем запрос к реальному URL, который через переменную окружения указывает на mock-server:
class PaymentIntegrationTest extends TestCase
{
private PaymentService $paymentService;
protected function setUp(): void
{
$this->paymentService = new PaymentService(
new HttpClient(['base_uri' => $_ENV['EXTERNAL_API_URL']])
);
}
public function testSuccessfulCharge(): void
{
$result = $this->paymentService->charge(100.50, 'USD');
$this->assertEquals('success', $result['status']);
$this->assertArrayHasKey('transaction_id', $result);
}
}
Важно: после выполнения тестов сбрасываем ожидания MockServer, чтобы не было перекрёстного влияния:
protected function tearDown(): void
{
$httpClient = new Client(['base_uri' => $_ENV['EXTERNAL_API_URL']]);
$httpClient->put('/mockserver/reset');
}
4. Параллельный запуск и работа с очередями
Для ускорения тестов используем параллельный запуск PHPUnit с помощью опции --parallel (требуется paratest):
docker-compose -f docker-compose.test.yml run --rm php-test \
./vendor/bin/paratest --processes=4 tests/Integration/
Однако при работе с очередями (например, RabbitMQ или Redis) нужно быть осторожным: параллельные тесты могут конкурировать за одни и те же сообщения. Решение — изолировать очереди для каждого процесса с помощью префиксов:
class QueueIntegrationTest extends TestCase
{
private string $queueName;
protected function setUp(): void
{
// Уникальный префикс на основе ID процесса
$this->queueName = 'test_queue_' . getmypid();
}
public function testMessageProcessing(): void
{
$queueService = new QueueService($_ENV['REDIS_URL']);
$queueService->send($this->queueName, ['data' => 'test']);
// Обработка
$result = $queueService->receive($this->queueName);
$this->assertNotNull($result);
}
}
Также не забывайте очищать очереди в tearDown().
5. Интеграция с CI/CD (GitLab CI пример)
Финальный этап — автоматизация тестов в пайплайне. Пример конфигурации .gitlab-ci.yml:
stages:
- test
integration-tests:
stage: test
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script: