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

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

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

Начать курс

Интеграционное тестирование 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:                

Блоги

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