Введение: почему интеграционное тестирование PHP в Docker — это must-have
Разработка современных PHP-приложений редко ограничивается одним сервером. Базы данных, кэши (Redis, Memcached), очереди (RabbitMQ), внешние API — всё это требует проверки взаимодействия компонентов. Модульные тесты (unit tests) изолируют код, но не гарантируют, что класс A правильно работает с классом B через реальную базу данных. Здесь на помощь приходит интеграционное тестирование.
Проблема классического подхода — настройка окружения. Разработчик тратит часы на поднятие MySQL, настройку прав, установку расширений PHP. Docker решает эту проблему раз и навсегда. В этой статье мы разберём, как организовать интеграционное тестирование PHP-приложения с использованием Docker и Docker Compose. Вы получите готовые конфиги, примеры кода и рекомендации по оптимизации.
1. Настройка Docker-окружения для PHP-тестов
1.1. Базовая структура проекта
Предположим, у нас есть простое PHP-приложение на Symfony или Laravel (подойдёт любой фреймворк). Создадим файл docker-compose.yml в корне проекта:
version: '3.8'
services:
php:
build: .
volumes:
- .:/var/www/html
environment:
- DATABASE_URL=mysql://test_user:test_pass@db:3306/test_db
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root_pass
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_pass
ports:
- "3307:3306" # Проброс на нестандартный порт, чтобы не конфликтовать с локальной БД
test_runner:
image: php:8.2-cli
volumes:
- .:/var/www/html
working_dir: /var/www/html
entrypoint: ["vendor/bin/phpunit"]
depends_on:
- db
- php
Ключевые моменты:
- Сервис
test_runnerиспользует отдельный контейнер с PHP CLI — это изолирует выполнение тестов от основного приложения. - База данных поднимается с чистой схемой. Для каждого прогона тестов мы можем пересоздавать её.
- Порт MySQL проброшен на
3307, чтобы не мешать локальной БД разработчика.
1.2. Dockerfile для PHP-приложения
Создадим Dockerfile для основного сервиса php:
FROM php:8.2-fpm
RUN docker-php-ext-install pdo_mysql mysqli
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-interaction --prefer-dist
Этот образ будет использоваться как для разработки, так и для тестов. Важно установить расширение pdo_mysql — без него тесты, работающие с БД, упадут.
2. Написание интеграционных тестов на PHPUnit
2.1. Базовая настройка PHPUnit
Убедитесь, что у вас установлен PHPUnit (через Composer). Файл phpunit.xml должен указывать на тестовую базу данных:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<php>
<env name="DATABASE_URL" value="mysql://test_user:test_pass@db:3306/test_db" />
<env name="APP_ENV" value="test" />
</php>
<testsuites>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
</phpunit>
2.2. Пример интеграционного теста: работа с БД
Допустим, у нас есть репозиторий пользователей. Напишем тест, который проверяет сохранение и поиск:
<?php
namespace App\Tests\Integration;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class UserRepositoryTest extends KernelTestCase
{
private ?EntityManagerInterface $entityManager;
private ?UserRepository $userRepository;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
$this->userRepository = $this->entityManager->getRepository(User::class);
// Очищаем таблицу перед каждым тестом
$this->entityManager->createQuery('DELETE FROM App\Entity\User')->execute();
}
public function testSaveAndFindUser(): void
{
$user = new User();
$user->setEmail('test@example.com');
$user->setName('Test User');
$this->entityManager->persist($user);
$this->entityManager->flush();
$foundUser = $this->userRepository->findOneBy(['email' => 'test@example.com']);
$this->assertNotNull($foundUser);
$this->assertEquals('Test User', $foundUser->getName());
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null;
}
}
Важно: В методе setUp() мы очищаем БД. Это гарантирует, что каждый тест начинается с чистого состояния. Для больших проектов используйте транзакции с откатом — это быстрее, чем полная очистка таблиц.
3. Запуск тестов и управление окружением
3.1. Команды для запуска
Соберём и запустим контейнеры:
docker-compose up -d --build
docker-compose run --rm test_runner
Первая команда поднимает сервисы. Вторая запускает PHPUnit внутри контейнера test_runner. Флаг --rm удаляет контейнер после завершения тестов — это экономит место.
Чтобы пересоздать базу данных перед тестами, добавьте скрипт bin/reset-test-db.sh: