Тестирование в PHP: Unit, Integration, E2E на продвинутом уровне

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

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

Начать курс

Тестирование в PHP: Unit, Integration, E2E на продвинутом уровне

В современной веб-разработке на PHP качество кода перестало быть опцией — это обязательное условие выживания проекта. Базовые юнит-тесты знакомы каждому разработчику, но как только проект перерастает рамки "Hello World", возникает необходимость в более сложных подходах: интеграционных тестах, проверке взаимодействия компонентов и сквозном тестировании пользовательских сценариев (E2E).

Эта статья предназначена для PHP-разработчиков уровня Middle и выше, которые уже знакомы с PHPUnit и хотят углубить свои знания. Мы разберем продвинутые техники: тестирование с внешними зависимостями, работу с тестовыми контейнерами, изоляцию инфраструктуры и построение полноценной стратегии тестирования.

1. Продвинутый Unit-тестинг: Mock-объекты и Stub-заглушки

На продвинутом уровне юнит-тесты перестают быть просто проверкой математических функций. Они тестируют бизнес-логику, изолированную от внешнего мира. Ключевые инструменты — Mock и Stub.

Stub возвращает предопределенные данные. Mock проверяет, как именно были вызваны методы (количество вызовов, порядок аргументов).

Рассмотрим пример: сервис оплаты, который отправляет запрос в платежный шлюз.

<?php
class PaymentService {
    public function __construct(
        private PaymentGatewayInterface $gateway,
        private LoggerInterface $logger
    ) {}

    public function processPayment(Order $order): bool {
        $result = $this->gateway->charge(
            $order->getAmount(),
            $order->getCurrency()
        );

        if ($result->isSuccess()) {
            $this->logger->info('Payment processed', ['order_id' => $order->getId()]);
            return true;
        }

        $this->logger->warning('Payment failed', ['error' => $result->getError()]);
        return false;
    }
}

// Тест с использованием PHPUnit
class PaymentServiceTest extends TestCase {
    public function testSuccessfulPaymentLogsInfo(): void {
        // Создаем Stub для шлюза
        $gatewayStub = $this->createStub(PaymentGatewayInterface::class);
        $gatewayStub->method('charge')
            ->willReturn(new PaymentResult(true));

        // Создаем Mock для логгера (проверяем вызов)
        $loggerMock = $this->createMock(LoggerInterface::class);
        $loggerMock->expects($this->once())
            ->method('info')
            ->with('Payment processed', $this->arrayHasKey('order_id'));

        $service = new PaymentService($gatewayStub, $loggerMock);
        $result = $service->processPayment(new Order(100, 'USD'));

        $this->assertTrue($result);
    }
}

Продвинутая техника: используйте willReturnCallback() для динамических ответов и withConsecutive() для проверки последовательности вызовов.

2. Интеграционное тестирование: База данных и внешние API

Интеграционные тесты проверяют, как ваш код взаимодействует с реальными компонентами: базой данных, файловой системой, кэшем. Здесь нельзя полагаться на моки — нужна настоящая инфраструктура.

Проблема: тесты становятся медленными и хрупкими. Решение: использовать тестовые контейнеры (Testcontainers) или встроенные SQLite-базы.

Пример интеграционного теста для репозитория пользователей с Doctrine:

<?php
class UserRepositoryIntegrationTest extends KernelTestCase {
    private EntityManagerInterface $entityManager;
    private UserRepository $repository;

    protected function setUp(): void {
        self::bootKernel();
        $this->entityManager = self::$kernel->getContainer()
            ->get('doctrine')
            ->getManager();
        
        // Создаем схему базы данных в памяти (SQLite)
        $schemaTool = new SchemaTool($this->entityManager);
        $schemaTool->createSchema([
            $this->entityManager->getClassMetadata(User::class)
        ]);
        
        $this->repository = $this->entityManager
            ->getRepository(User::class);
    }

    public function testFindActiveUsers(): void {
        // Arrange: добавляем тестовые данные
        $activeUser = new User('active@test.com', true);
        $inactiveUser = new User('inactive@test.com', false);
        
        $this->entityManager->persist($activeUser);
        $this->entityManager->persist($inactiveUser);
        $this->entityManager->flush();

        // Act
        $activeUsers = $this->repository->findActiveUsers();

        // Assert
        $this->assertCount(1, $activeUsers);
        $this->assertEquals('active@test.com', $activeUsers[0]->getEmail());
    }

    protected function tearDown(): void {
        $this->entityManager->close();
        parent::tearDown();
    }
}

Совет: для тестирования внешних API используйте WireMock или Guzzle Mock Handler. Это позволит эмулировать HTTP-ответы без реальных запросов.

3. E2E-тестирование: Сквозные сценарии через браузер

E2E-тесты (End-to-End) имитируют действия реального пользователя. Для PHP-проектов популярны два подхода:

  • Codeception — фреймворк, объединяющий Unit, Integration и E2E тесты.
  • Symfony Panther — мощный инструмент для E2E на базе ChromeDriver.

Пример сценария для интернет-магазина с использованием Panther:

<?php
class CheckoutE2ETest extends PantherTestCase {
    public function testUserCanCompletePurchase(): void {
        $client = static::createPantherClient();
        
        // 1. Переход на главную страницу
        $crawler = $client->request('GET', '/');
        $this->assertSelectorTextContains('h1', 'Интернет-магазин');

        // 2. Добавление товара в корзину
        $client->clickLink('Купить');
        $client->waitFor('.cart-total');
        $this->assertSelectorTextContains('.cart-total', '1 това                

Блоги

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