fachlich sauber trennen
Wer jeden Collaborator blind als Mock behandelt, verliert die semantische Aussagekraft seiner Tests. Stub, Fake, Spy, Mock und Dummy haben klar definierte Rollen – und wer diese Rollen verwechselt, schreibt Tests, die Fehler verbergen statt aufzudecken.
Inhaltsverzeichnis
- 1. Warum die Unterscheidung von Test Doubles wichtig ist
- 2. Dummy: das unbedeutende Platzhalter-Objekt
- 3. Stub: kontrollierte Rückgabewerte ohne Verifikation
- 4. Fake: funktionale Implementierung als Ersatz
- 5. Spy: aufzeichnen statt verifizieren
- 6. Mock: Verhaltensverifikation mit Erwartungen
- 7. PHPUnit-API für Test Doubles im Überblick
- 8. Typische Fehler beim Einsatz von Test Doubles
- 9. Vergleich: wann welcher Test Double?
- 10. Zusammenfassung
- 11. FAQ
1. Warum die Unterscheidung von Test Doubles wichtig ist
Das Vokabular um Test Doubles stammt ursprünglich aus Gerard Meszaros' Buch „xUnit Test Patterns". Dort wird klar zwischen fünf verschiedenen Arten unterschieden: Dummy, Stub, Fake, Spy und Mock. In der PHP-Praxis hat sich jedoch eine problematische Sprachunschärfe eingebürgert: Entwickler nennen jeden Collaborator-Ersatz einfach „Mock", egal welche Semantik dieser erfüllt. Das führt dazu, dass Tests ungewollt mehr verifizieren als beabsichtigt – oder im Gegenteil weniger als nötig, weil Stubs für Mocks gehalten werden und keine Erwartungen formuliert werden.
Die fachliche Trennung hat praktische Konsequenzen: Ein Test, der einen echten Mock mit expects($this->once()) verwendet, schlägt fehl, wenn die getestete Methode den Collaborator gar nicht aufruft. Ein Stub ohne Erwartung hingegen schlägt niemals aufgrund fehlender Aufrufe fehl. Wer beide Begriffe vermischt, schreibt entweder Tests, die zu fragil sind und bei jeder Refaktorierung brechen, oder Tests, die zu permissiv sind und tatsächliche Bugs durchlassen. Das Verstehen dieser Unterschiede ist der erste Schritt zu einer Testsuite, die wirklich Sicherheit gibt.
PHPUnit bietet mit createMock(), createStub(), getMockBuilder() und seit PHPUnit 10 mit createMockForIntersectionOfInterfaces() eine API, die diese Unterscheidungen teilweise sprachlich unterstützt. Dennoch liegt die Verantwortung beim Entwickler, das richtige Werkzeug für die jeweilige Situation auszuwählen. Dieser Artikel erklärt jeden Test-Double-Typ mit konkreten Beispielen aus der PHP-Praxis.
2. Dummy: das unbedeutende Platzhalter-Objekt
Ein Dummy ist ein Objekt, das nur existiert, um eine Parameterliste zu befüllen. Es wird nie aufgerufen, seine Methoden werden nie ausgeführt. Das klingt trivial, aber Dummies haben einen wichtigen Platz in der Testsuite: Sie machen deutlich, dass ein bestimmter Collaborator für den getesteten Pfad irrelevant ist. Wenn der Konstruktor eines Service drei Dependencies erwartet, aber der zu testende Pfad nur eine davon nutzt, sind die anderen beiden Dummies.
In PHPUnit erstellt man einen Dummy am einfachsten mit $this->createMock(Interface::class) und konfiguriert ihn nicht weiter – keine Return-Werte, keine Erwartungen. Der Unterschied zum expliziten Stub: Ein Dummy-Objekt, das unerwartet aufgerufen wird, gibt null zurück, ohne einen Fehler zu werfen. Das ist gewollt, wenn der Aufruf im Testpfad schlicht nicht stattfindet. Wer Dummies sauber benennt – zum Beispiel als $unusedLogger oder $dummyEventDispatcher – kommuniziert im Test direkt, welche Abhängigkeiten für diesen Fall irrelevant sind.
3. Stub: kontrollierte Rückgabewerte ohne Verifikation
Ein Stub liefert vorgefertigte Antworten auf Methodenaufrufe. Der entscheidende Unterschied zum Mock: Ein Stub macht keine Aussage darüber, ob oder wie oft seine Methoden aufgerufen werden. Er liefert nur den konfigurierten Rückgabewert, wenn er aufgerufen wird. PHPUnit-Stubs werden mit createStub() (seit PHPUnit 9) oder mit createMock() plus method()->willReturn() erstellt. createStub() ist dabei die semantisch sauberere Wahl, weil es im Testcode klar signalisiert, dass keine Interaktionsverifikation stattfindet.
Stubs eignen sich ideal für Query-Methoden – also für Methoden, die Daten liefern, ohne Seiteneffekte auszulösen. Ein Repository, das im Test eine feste Produktliste zurückgibt, ist ein klassischer Stub. Ein Preisrechner-Service, der immer denselben Preis zurückgibt, ist ein Stub. Wichtig: Stubs sollten keine Erwartungen formulieren. Sobald man expects($this->once()) oder expects($this->exactly(2)) hinzufügt, wird der Stub zum Mock – und damit ändert sich die semantische Aussage des Tests grundlegend.
<?php
// Stub: provides controlled return values — no interaction verification
use PHPUnit\Framework\TestCase;
class PriceCalculatorTest extends TestCase
{
public function testCalculatesTotalWithTaxFromStub(): void
{
// Stub: we only care about the return value, not HOW OFTEN it's called
$taxProvider = $this->createStub(TaxProviderInterface::class);
$taxProvider->method('getTaxRate')
->willReturn(0.19);
$calculator = new PriceCalculator($taxProvider);
$total = $calculator->calculateGross(100.00);
// Assert state — not interaction
$this->assertSame(119.00, $total);
}
public function testReturnsZeroForEmptyCart(): void
{
// Dummy: taxProvider won't be called at all for empty cart
$unusedTaxProvider = $this->createStub(TaxProviderInterface::class);
$calculator = new PriceCalculator($unusedTaxProvider);
$total = $calculator->calculateGross(0.00);
$this->assertSame(0.00, $total);
}
}
4. Fake: funktionale Implementierung als Ersatz
Ein Fake ist eine vollwertige, aber vereinfachte Implementierung einer Schnittstelle. Anders als Stubs und Mocks wird ein Fake nicht durch die PHPUnit-Mock-API erstellt, sondern als echte PHP-Klasse geschrieben. Der klassische Fake ist ein In-Memory-Repository: Es implementiert vollständig das Repository-Interface mit save(), findById() und findAll(), speichert die Daten aber in einem Array statt in einer Datenbank. Dadurch können komplexe Szenarien getestet werden, ohne dass Mocks aufwendig konfiguriert werden müssen.
Fakes sind besonders wertvoll für Integrationstests und für Szenarien, bei denen mehrere Methoden desselben Collaborators aufgerufen werden und die Zustandsübergänge korrekt abgebildet werden müssen. Ein Mock-Repository, das bei jedem findById()-Aufruf fest einen Wert zurückgibt, kann nicht testen, ob ein zuvor gespeichertes Objekt korrekt abgerufen wird. Ein Fake-Repository kann das, weil es wirklich speichert und abruft. Der Nachteil: Fakes müssen gepflegt werden, wenn sich das Interface ändert. Für langlebige Tests in größeren Projekten lohnt sich dieser Aufwand jedoch sehr.
5. Spy: aufzeichnen statt verifizieren
Ein Spy ist ein Test Double, das Aufrufe aufzeichnet, ohne sie vorab zu erwarten. Im Gegensatz zum Mock formuliert man beim Spy keine Erwartungen vor dem Test-Durchlauf, sondern überprüft die aufgezeichneten Aufrufe danach. PHPUnit unterstützt das Spy-Muster nicht nativ als eigene API, aber es lässt sich elegant mit einem getMockBuilder()-Objekt umsetzen, das Aufrufe über eine öffentliche Variable sammelt. Alternativ nutzt man Bibliotheken wie Mockery, die Spies explizit unterstützen.
In der Praxis ist der Spy besonders nützlich, wenn man testen möchte, welche Argumente bei einem bestimmten Aufruf übergeben wurden, ohne den genauen Aufrufzeitpunkt zu fixieren. Ein Event-Dispatcher als Spy zeichnet alle dispatched Events auf – nach dem Test kann dann überprüft werden, ob das richtige Event mit den richtigen Daten gesendet wurde. Das ist weniger restriktiv als ein Mock mit expects($this->once())->with($this->isInstanceOf(OrderCreatedEvent::class)), weil der Spy auch dann sinnvolle Assertions erlaubt, wenn die Reihenfolge oder Anzahl der Aufrufe nebensächlich ist.
6. Mock: Verhaltensverifikation mit Erwartungen
Ein Mock ist das restriktivste Test Double: Es formuliert vor dem Testlauf explizite Erwartungen darüber, welche Methoden wie oft mit welchen Argumenten aufgerufen werden. PHPUnit verifiziert diese Erwartungen automatisch am Ende des Tests. Wenn eine erwartete Methode nicht aufgerufen wird, schlägt der Test fehl – auch wenn alle Assertions innerhalb der Testmethode erfüllt sind. Das macht Mocks zum richtigen Werkzeug für Command-Methoden: Methoden, die Seiteneffekte auslösen und bei denen der korrekte Aufruf selbst der zu testende Aspekt ist.
Ein klassisches Mock-Szenario: Ein OrderService soll beim Abschließen einer Bestellung exakt einmal eine E-Mail senden. Hier ist der korrekte Aufruf des Mailers das, was getestet werden soll – nicht der Rückgabewert. Der Mock stellt sicher, dass send() genau einmal aufgerufen wird. Wenn der Entwickler versehentlich den Mailer-Aufruf entfernt, schlägt der Test fehl. Ein Stub würde das nicht erkennen, weil er keine Erwartungen kennt. Mocks sind damit das direkteste Werkzeug für die Verifikation von Command-Query-Separation auf der Unit-Test-Ebene.
<?php
// Mock: verifies that send() is called exactly once with the right argument
use PHPUnit\Framework\TestCase;
class OrderServiceTest extends TestCase
{
public function testSendsConfirmationEmailOnOrderCompletion(): void
{
// Mock: we EXPECT this method to be called — test fails if it isn't
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with($this->callback(function (Email $email): bool {
return $email->getTo() === 'customer@example.com'
&& str_contains($email->getSubject(), 'Bestellbestätigung');
}));
// Stub: we only need the order data, no interaction verification
$orderRepo = $this->createStub(OrderRepositoryInterface::class);
$orderRepo->method('findById')
->willReturn(new Order(id: 42, customerEmail: 'customer@example.com'));
$service = new OrderService($mailer, $orderRepo);
$service->complete(orderId: 42);
// PHPUnit automatically verifies mock expectations after the test
}
}
7. PHPUnit-API für Test Doubles im Überblick
PHPUnit 10 und 11 haben die Test-Double-API deutlich bereinigt. Die veraltete getMock()-Methode ist verschwunden, createMock() und createStub() sind die bevorzugten Einstiegspunkte. createMock() erzeugt ein Objekt, bei dem alle Methoden standardmäßig null zurückgeben und auf dem Erwartungen mit expects() formuliert werden können. createStub() erzeugt dasselbe Objekt, signalisiert aber semantisch, dass keine Interaktionsverifikation stattfindet – der Unterschied liegt im Ausdruck, nicht im Verhalten.
Für komplexere Szenarien bietet getMockBuilder() volle Kontrolle: Man kann den Konstruktor deaktivieren (disableOriginalConstructor()), nur bestimmte Methoden mocken (onlyMethods([])) oder zusätzliche Methoden hinzufügen (addMethods([])). Seit PHPUnit 10 ist willReturnCallback() der empfohlene Weg für dynamische Rückgabewerte, da returnCallback() als Standalone-Methode entfernt wurde. Die Methode willThrowException() ermöglicht das Simulieren von Exceptions, was für Fehlerbehandlungs-Tests unverzichtbar ist.
8. Typische Fehler beim Einsatz von Test Doubles
Der häufigste Fehler ist das Übermocken: Jede Dependency wird als Mock konfiguriert, auch wenn nur Rückgabewerte benötigt werden. Das führt zu Tests, die bei jeder internen Refaktorierung brechen, weil Mocks auf Implementierungsdetails (Aufrufanzahl, Reihenfolge) fixiert sind. Die Regel lautet: Mock nur, was unbedingt als Command verifiziert werden muss. Alles andere ist ein Stub oder ein Fake.
Ein zweiter Klassiker: Assertions im Mock statt im Testbody. Wenn with() zu komplex wird, verlagert man die Überprüfung besser in einen eigenen Assertion-Block nach dem Act-Schritt, indem man einen Spy-ähnlichen Ansatz mit einem Callback und einer gespeicherten Variable nutzt. Ein dritter Fehler: das Mocken von Wert-Objekten und Entities. Werte wie Money, Email oder OrderId sind keine Services – sie haben keine Abhängigkeiten und sollten immer als echte Objekte instanziiert werden. Wer sie mockt, testet PHPUnit statt seinen eigenen Code.
<?php
// Common mistake: over-mocking — mocking value objects and simple collaborators
class BadTest extends TestCase
{
public function testBad(): void
{
// WRONG: Money is a value object — create it for real
$price = $this->createMock(Money::class);
$price->method('getAmount')->willReturn(100);
// WRONG: using mock where stub is correct — no interaction to verify
$repo = $this->createMock(ProductRepository::class);
$repo->expects($this->once())->method('findById')->willReturn($product);
// If the implementation calls findById twice for caching, test breaks for wrong reason
}
}
class GoodTest extends TestCase
{
public function testGood(): void
{
// CORRECT: value object instantiated for real
$price = new Money(amount: 100, currency: 'EUR');
// CORRECT: stub — we need the return value, not interaction count
$repo = $this->createStub(ProductRepository::class);
$repo->method('findById')->willReturn(new Product(id: 1, price: $price));
$service = new PricingService($repo);
$result = $service->getDisplayPrice(productId: 1);
$this->assertSame('100,00 €', $result);
}
}
9. Vergleich: wann welcher Test Double?
Die Wahl des richtigen Test Doubles ist keine akademische Übung, sondern hat direkte Auswirkungen auf Wartbarkeit und Aussagekraft der Testsuite. Die folgende Tabelle fasst zusammen, welcher Test Double in welcher Situation der richtige ist.
| Test Double | Zweck | Verifiziert Interaktion? | Einsatzbeispiel |
|---|---|---|---|
| Dummy | Parameterliste befüllen | Nein | Irrelevante Dependency im Konstruktor |
| Stub | Feste Rückgabewerte liefern | Nein | Repository für Query-Test |
| Fake | Vollwertige Ersatzimplementierung | Nein | In-Memory-Repository für Integrationstests |
| Spy | Aufrufe aufzeichnen, nachher prüfen | Nachher | Event-Dispatcher auf ausgelöste Events prüfen |
| Mock | Aufrufe vorab erwarten und verifizieren | Ja, vorab | Mailer auf exakten Send-Aufruf prüfen |
Die Faustregel für die Wahl zwischen Mock und Stub orientiert sich an Command-Query-Separation: Queries (lesende Methoden ohne Seiteneffekte) werden gestubbt, Commands (Methoden mit Seiteneffekten) werden gemockt. Fakes kommen zum Einsatz, wenn mehrere Methoden desselben Interfaces zusammenspielen müssen und ein Stub die Komplexität nicht mehr beherrschbar macht. Dummies werden immer dann explizit benannt, wenn die Absicht kommuniziert werden soll, dass eine Dependency in diesem Testfall keine Rolle spielt.
Mironsoft
PHPUnit-Beratung, Test-Architektur und Code-Qualität für PHP-Projekte
Testsuites, die wirklich Sicherheit geben?
Wir analysieren bestehende PHPUnit-Tests, identifizieren Overmocking und falsch eingesetzte Test Doubles und refaktorieren die Testsuite zu einer wartbaren, aussagekräftigen Sammlung, auf die man sich bei Deployments verlässt.
Test-Review
Analyse der bestehenden Testsuite auf Overmocking, fehlende Assertions und falsch eingesetzte Test Doubles
Refactoring
Umstellen auf korrekte Test Doubles, Einführen von Fake-Repositories und saubere Trennung von Stub und Mock
Schulung
Team-Workshop zu Test Doubles, CQS im Testkontext und Testarchitektur für PHP-Projekte
10. Zusammenfassung
Die fachliche Trennung von Test Doubles ist kein akademisches Hobby, sondern ein praktisches Werkzeug für wartbare Testsuites. Dummies kommunizieren, welche Dependencies irrelevant sind. Stubs liefern kontrollierte Daten ohne Interaktionsverifikation – ideal für Query-Methoden. Fakes sind vollwertige Ersatzimplementierungen für komplexe Szenarien mit Zustandsübergängen. Spies zeichnen Aufrufe auf und erlauben nachträgliche Verifikation ohne vorab fixierte Erwartungen. Mocks verifizieren Command-Methoden mit exakten Erwartungen über Aufrufe, Anzahl und Argumente.
Der wichtigste Grundsatz: Command-Query-Separation als Leitfaden für die Wahl des Test Doubles nutzen. Queries werden gestubbt, Commands werden gemockt. Wer Fakes für komplexe Collaborators einführt, reduziert die Abhängigkeit von PHPUnits Mock-API und schreibt Tests, die stabiler gegenüber internen Refaktorierungen sind. Übermocken ist das größte Qualitätsproblem in vielen PHPUnit-Suiten – und das erste, was bei einem Test-Review auffällt.
Test Doubles in PHPUnit — Das Wichtigste auf einen Blick
Stub vs. Mock
Stub liefert Daten ohne Erwartungen. Mock verifiziert Command-Aufrufe vorab. Nie Mocks für Query-Methoden verwenden – das macht Tests fragil.
Fake für Komplexität
In-Memory-Repositories als Fakes schreiben, wenn mehrere Interface-Methoden zusammenspielen. Wartbarer als komplizierte Mock-Konfigurationen.
createStub() vs. createMock()
createStub() seit PHPUnit 9 für Stubs bevorzugen — kommuniziert semantisch, dass keine Interaktionsverifikation stattfindet.
Kein Mocken von Value Objects
Money, Email, OrderId und andere Wert-Objekte immer als echte Instanzen erstellen. Wer sie mockt, testet PHPUnit statt eigenen Code.