Интеграционное тестирование PHP с Docker: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Интеграционное тестирование 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 раза меньше по размеру и быстрее загру

Блоги

Рекомендаций по книгам