Testen in PHP: Unit, Integration, E2E auf fortgeschrittenem Niveau
In der modernen Webentwicklung mit PHP ist Codequalität keine Option mehr – sie ist eine zwingende Voraussetzung für das Überleben eines Projekts. Basale Unit-Tests sind jedem Entwickler bekannt, aber sobald ein Projekt den Rahmen von "Hello World" sprengt, entsteht der Bedarf an komplexeren Ansätzen: Integrationstests, Überprüfung der Komponenteninteraktion und End-to-End-Tests (E2E) von Benutzerszenarien.
Dieser Artikel richtet sich an PHP-Entwickler auf Middle-Niveau und höher, die bereits mit PHPUnit vertraut sind und ihr Wissen vertiefen möchten. Wir behandeln fortgeschrittene Techniken: Testen mit externen Abhängigkeiten, Arbeiten mit Testcontainern, Isolierung der Infrastruktur und Aufbau einer umfassenden Teststrategie.
1. Fortgeschrittenes Unit-Testen: Mock-Objekte und Stub-Platzhalter
Auf fortgeschrittenem Niveau sind Unit-Tests nicht mehr nur die Überprüfung mathematischer Funktionen. Sie testen die Geschäftslogik, isoliert von der Außenwelt. Die wichtigsten Werkzeuge sind Mock und Stub.
Stub gibt vordefinierte Daten zurück. Mock überprüft, wie Methoden aufgerufen wurden (Anzahl der Aufrufe, Reihenfolge der Argumente).
Betrachten wir ein Beispiel: ein Zahlungsdienst, der eine Anfrage an ein Zahlungsgateway sendet.
<?phpclass 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; }}
// Test mit PHPUnitclass PaymentServiceTest extends TestCase { public function testSuccessfulPaymentLogsInfo(): void { // Stub für das Gateway erstellen $gatewayStub = $this->createStub(PaymentGatewayInterface::class); $gatewayStub->method('charge') ->willReturn(new PaymentResult(true));
// Mock für den Logger erstellen (Aufruf überprüfen) $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); }}Fortgeschrittene Technik: Verwenden Sie willReturnCallback() für dynamische Antworten und withConsecutive() zur Überprüfung der Aufrufreihenfolge.
2. Integrationstests: Datenbank und externe APIs
Integrationstests überprüfen, wie Ihr Code mit realen Komponenten interagiert: Datenbank, Dateisystem, Cache. Hier kann man sich nicht auf Mocks verlassen – echte Infrastruktur ist nötig.
Problem: Tests werden langsam und anfällig. Lösung: Verwenden Sie Testcontainer (Testcontainers) oder eingebaute SQLite-Datenbanken.
Beispiel eines Integrationstests für ein Benutzer-Repository mit Doctrine:
<?phpclass UserRepositoryIntegrationTest extends KernelTestCase { private EntityManagerInterface $entityManager; private UserRepository $repository;
protected function setUp(): void { self::bootKernel(); $this->entityManager = self::$kernel->getContainer() ->get('doctrine