Тестирование в 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 това