Warum Tests ohne Mock-Objekte wie Kochen ohne Salz sind
\n\nStell dir vor, du entwickelst eine Anwendung, die E-Mail-Benachrichtigungen versendet, mit einer Datenbank arbeitet und eine externe API aufruft. Wie testest du das ohne Mock-Objekte und Fixtures in PHP? Du würdest jedes Mal echte Mails verschicken, Testdatensätze in der DB anlegen und auf die Antwort des externen Dienstes warten. Das ist langsam, unzuverlässig und schlichtweg ineffizient.
\n\nHier kommen die Test Doubles ins Spiel – Stellvertreter für reale Objekte. Im PHP-Ökosystem nutzt man dafür eine Kombination aus: Mock-Objekten (Mocks) zur Überprüfung von Interaktionen und Fixtures zur Vorbereitung der Testdaten. Schauen wir uns an, wie das in der Praxis funktioniert.
\n\nMock vs. Stub vs. Fake: Die Unterschiede verstehen
\n\nBevor wir in den Code eintauchen, klären wir die Begriffe. In der Welt des PHP-Unit-Testings gibt es drei Haupttypen von Stellvertretern:
\n\n- \n
- Stub – gibt vordefinierte Daten zurück. Du prüfst, ob dein Code diese Daten korrekt verarbeitet. \n
- Mock – gibt nicht nur Daten zurück, sondern prüft auch, wie dein System Methoden aufruft: wie oft, mit welchen Argumenten, in welcher Reihenfolge. \n
- Fake – eine vereinfachte, funktionierende Implementierung (z. B. eine In-Memory-Datenbank). \n
In PHPUnit, dem De-facto-Standard für PHP-Tests, werden diese Begriffe oft vermischt. Aber wir bleiben präzise: Mock-Objekte in PHP sind diejenigen, bei denen du Erwartungen (Expectations) definierst.
\n\nDein erstes Mock-Objekt mit PHPUnit
\n\nAngenommen, wir haben eine Klasse Mailer, die E-Mails versendet. Wir möchten prüfen, ob der Registrierungsdienst die Methode send() genau einmal mit den richtigen Daten aufruft.
<?php\nuse PHPUnit\\Framework\\TestCase;\n\nclass UserRegistrationTest extends TestCase\n{\n public function testSendWelcomeEmailOnRegistration(): void\n {\n // Mock für Mailer erstellen\n $mailer = $this->createMock(Mailer::class);\n \n // Erwartung definieren: Methode send wird 1x aufgerufen\n $mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->equalTo('user@example.com'),\n $this->stringContains('Welcome')\n );\n \n $service = new UserRegistrationService($mailer);\n $service->register('user@example.com', 'password123');\n }\n}\n\n\nBeachte: Wir prüfen nicht, ob die E-Mail tatsächlich versendet wurde. Wir prüfen, ob unser Code versucht hat, sie zu senden – das ist die Essenz von Mock-Objekten in PHP-Tests. Wird send() nicht aufgerufen (oder zweimal), schlägt der Test fehl.
Die Methode expects und ihre Möglichkeiten
\n\nPHPUnit bietet eine umfangreiche API zur Definition von Erwartungen:
\n\n- \n
$this->once()– genau ein Aufruf \n $this->exactly(3)– genau drei Aufrufe \n $this->atLeastOnce()– mindestens ein Aufruf \n $this->never()– die Methode darf nicht aufgerufen werden \n
Für Argumente verwendest du Matcher (Constraints):
\n\n$mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->callback(function($email) {\n return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;\n }),\n $this->anything() // Das zweite Argument ist uns egal\n );\n\n\nFixtures in PHP: Daten bereitstellen, bevor sie gebraucht werden
\n\nFixtures sind vordefinierte Datenzustände, in denen sich das System vor der Testausführung befindet. In PHPUnit werden dafür die Methoden setUp() und