Rätselhafte Objekte
Meine Leidenschaft für das Programmieren entdeckte ich 1990 auf meinem ersten Computer, einem Amiga 500. Nach den ersten Schritten mit AmigaBASIC lernte ich schnell Assembler und C. Obwohl ich auch Programmiersprachen wie C++, AmigaE oder Oberon ausprobierte, spielte die Objektorientierung für mich damals keine große Rolle.
In einem Praktikum, das ich vor Beginn meines Informatikstudiums am GMD-Forschungszentrum Informationstechnik absolvierte, "musste" ich eine Anwendung in
Im Wintersemester 1998/99 begann ich mein Studium der Informatik an der Universität Bonn. Die Programmieraufgaben für die Vorlesung "Informatik I" sollten in Java implementiert werden. Weder der Professor noch die studentischen Hilfskräfte, die die Aufgaben korrigierten, legten Wert auf Objektorientierung. Für sie war Java, das erst ab meinem Jahrgang in der Lehre eingesetzt wurde, lediglich das neue Modula-2.
Glücklicherweise war ein Mitglied der Fachschaft Informatik mit dieser Situation so unzufrieden, dass es einen Java-Kurs für Interessierte anbot. Dort begann ich allmählich zu erahnen, worum es bei der Objektorientierung geht. Bei den Hausarbeiten im Grundstudium half mir das jedoch nicht, da ich schnell lernen musste, dass sauber programmierte Lösungen nicht unbedingt akzeptiert wurden. Im Hauptstudium traf ich im Fachbereich Softwaretechnik dann auf engagierte und kompetente Menschen, von denen ich unter anderem viel über Objektorientierung und Design Patterns lernte.
Von Werten zu Services
Erst mit der Zeit und zunehmender Erfahrung habe ich erkannt, dass nicht alle Objekte gleich sind.
Wertobjekte (Value Objects) haben keine eindeutige Identität und repräsentieren Werte, beispielsweise ein Datum oder einen Geldbetrag. Zwei Wertobjekte derselben Klasse, die für alle ihre Eigenschaften die gleichen Werte haben, werden als identisch betrachtet.
Entitäten (Entities) sind Objekte mit einer eindeutigen Identität, die über Zeit und Zustandsänderungen hinweg bestehen bleibt. Sie repräsentieren zentrale Geschäftsobjekte der Domäne und haben einen eigenen Lebenszyklus. Idealerweise werden diese Zustandsänderungen explizit als Ereignisse protokolliert, sodass der aktuelle Zustand der Entität aus diesen Ereignissen wiederhergestellt werden kann.
Services kapseln Geschäftslogik, die nicht natürlich zu einer Entität oder einem Wertobjekt gehört. Es handelt sich dabei um zustandslose Klassen, die komplexe Operationen koordinieren oder domänenspezifische Berechnungen durchführen:
- Domain Services enthalten domänenspezifische Geschäftslogik und arbeiten mit mehreren Entitäten.
- Application Services koordinieren zwischen Domain Layer und Infrastructure. Sie enthalten keine Geschäftslogik und delegieren an Domain Objects.
Im Kontext von Domain-Driven Design (DDD) sind Wertobjekte, Entitäten und Services fundamentale Bausteine zur Strukturierung komplexer Softwaresysteme.
Test Doubles
Test Doubles sind ein grundlegendes Konzept im Software-Testing, dessen Terminologie maßgeblich von Gerard Meszaros in seinem einflussreichen Buch "xUnit Test Patterns" geprägt wurde.
Ein Test Stub sieht aus wie ein echtes kollaborierendes Objekt (eine echte Abhängigkeit), kann konfiguriert werden, um Werte zurückzugeben oder Exceptions zu werfen. Es ermöglicht, Code zu testen, der mit dem durch einen Test Stub ersetzten Objekt interagiert, ohne dass der Code der echten Abhängigkeit ausgeführt wird.
Meszaros definiert einen Test Stub als Ersatz für eine echte Komponente, von der das System Under Test (SUT) abhängt. Dadurch erhält der Test einen Kontrollpunkt für die indirekten Eingaben des SUT. So kann der Test das SUT in die Ausführungspfade zwingen, die in einem Test verifiziert werden sollen.
Ein Mock Object ist ein Test Stub, der so konfiguriert werden kann, dass er Nachrichten (Methodenaufrufe) erwartet. Dadurch ist es möglich, die Kommunikation zwischen Objekten zu testen.
Ein Mock Object wird als Beobachtungspunkt verwendet, um die indirekten Ausgaben des Systems Under Test (SUT) während seiner Ausführung zu verifizieren. Ein Mock Object beinhaltet auch die Funktionalität eines Test Stubs, da es Werte an das SUT zurückgeben muss. Der Schwerpunkt liegt jedoch auf der Verifikation der indirekten Ausgaben, also der Kommunikation zwischen dem SUT und dem durch das Mock Object ersetzten Objekt.
Services implementieren ein Interface, das durch ein Test Double ersetzt werden kann:
- Wir verwenden eine echte Implementierung dieses Interfaces, wenn wir diese konkrete Implementierung testen möchten.
- Wir verwenden einen Test Stub dieses Interfaces, wenn wir einen anderen Teil des Systems testen möchten, der von diesem Interface abhängt.
- Wir verwenden ein Mock Object dieses Interfaces, wenn wir die Kommunikation zwischen kollaborierenden Objekten testen möchten.
Gerard Meszaros' systematische Herangehensweise an Test Doubles hat das Vokabular des modernen Software-Testings geprägt. Seine klare Unterscheidung zwischen den verschiedenen Arten von Ersatzobjekten, die beim Testen verwendet werden, basiert nicht nur auf ihrer technischen Implementierung, sondern auch darauf, wie und warum wir diese Test Doubles verwenden.
Diese Terminologie ist heute in der Softwareentwicklung Standard und hilft Teams, präzise über ihre Teststrategien zu kommunizieren und die passende Art von Test Double für spezifische Testanforderungen auszuwählen.
Hier ist eine Aufnahme von einem Vortrag, in dem ich das erkläre:
Der lange Weg zu PHPUnit 12
Die Geschichte der Test Doubles in PHPUnit ist eine Reise von anfänglicher Verwirrung zu Klarheit. Diese Entwicklung spiegelt nicht nur die technische Evolution des Frameworks wider, sondern auch ein tieferes Verständnis für die verschiedenen Arten von Test Doubles und deren angemessene Verwendung. Über dieses Thema habe ich bereits weiter oben geschrieben.
Als PHPUnit im Jahr 2006 erstmals Unterstützung für Test Doubles erhielt, war dieses Konzept für die PHP-Community revolutionär. Damals kannte PHPUnit jedoch nur eine einzige Methode zur Erzeugung von Test Doubles: getMock(). Diese Methode war ein Allzweckwerkzeug, das sowohl Test Stubs als auch Mock Objects erzeugen konnte, ohne zwischen diesen beiden grundlegend unterschiedlichen Konzepten zu differenzieren.
Diese mangelnde Unterscheidung führte zu erheblichen Problemen beim Lesen und Verstehen von Testcode. Es war nicht offensichtlich, ob ein Test Stub zur Entkopplung von Abhängigkeiten oder ein Mock Object zur Verifikation der Kommunikation zwischen Objekten erstellt wurde. Die Intention blieb verborgen, was die Wartbarkeit und Verständlichkeit der Tests erheblich beeinträchtigte.
Ein weiteres gravierendes Problem war die wachsende Komplexität der Methode getMock(). Im Laufe der Jahre erhielt sie zahlreiche zusätzliche optionale Parameter, was ihre Verwendung zunehmend unübersichtlich machte. Besonders problematisch war, dass häufig einer der letzten Parameter verwendet werden musste, um die Ausführung des Konstruktors der zu ersetzenden Klasse zu verhindern. Diese Parameterjonglage machte den Code schwer lesbar und fehleranfällig.
Der erste Versuch, diese API-Probleme zu lösen, war die Einführung des Mock Builders. Dieser bot eine Fluent API, die es ermöglichte, Test Doubles mit einer flexibleren Syntax zu erstellen. Allerdings löste der Mock Builder das grundlegende Problem nicht, da auch mit dieser API keine klare Unterscheidung zwischen Test Stub und Mock Object möglich war.
Ein bedeutender Schritt in Richtung Klarheit war die Einführung der Methode createMock() als Alternative zu getMock(). Diese neue Methode vereinfachte die API erheblich, da sie nur noch ein einziges Argument benötigte: den Namen des Interfaces oder der Klasse, für die ein Test Double erstellt werden sollte. Die Methode bot mehrere Vorteile: eine wesentlich einfachere Syntax, ein besseres Standardverhalten und eine reduzierte Komplexität bei der Verwendung. Dennoch blieb ein Problem bestehen: Auch diese Methode erzeugte Objekte, die sowohl als Test Stub als auch als Mock Object verwendet werden konnten, ohne eine klare semantische Trennung vorzunehmen.
Mit der Einführung von createStub() in PHPUnit 8.4 begann ich, die wichtige Unterscheidung zwischen Test Stubs und Mock Objects endlich zu adressieren. Diese neue Methode war speziell dafür gedacht, Test-Stubs zu erzeugen, also Objekte, die lediglich als Platzhalter für Abhängigkeiten dienen und definierte Rückgabewerte liefern.
Die Intention hinter createStub() war klar: Diese Methode sollte verwendet werden, wenn der zu testende Code von einer Abhängigkeit entkoppelt werden sollte, kontrollierte Rückgabewerte für Testszenarien benötigt wurden und die Kommunikation zwischen den beteiligten Objekten nicht getestet werden sollte.
Gleichzeitig sollte createMock() von nun an ausschließlich für echte Mock Objects verwendet werden. Das sind Objekte, auf denen Erwartungen über Methodenaufrufe konfiguriert werden können, um die Kommunikation zwischen kollaborierenden Objekten zu testen.
Trotz der Einführung von createStub() bestand ein Problem fort: Aus Gründen der Abwärtskompatibilität erzeugte createStub() bis einschließlich PHPUnit 11 noch immer Objekte, die die API für die Konfiguration von Erwartungen besaßen. Das heißt, die semantische Trennung war zwar in der Intention vorhanden, wurde technisch aber noch nicht vollständig durchgesetzt. Diese Übergangsphase war notwendig, um bestehende Tests nicht zu beeinträchtigen. Sie führte jedoch dazu, dass die gewünschte Klarheit noch nicht vollständig erreicht wurde. Entwicklerinnen und Entwickler konnten weiterhin versehentlich Erwartungen auf Objekten konfigurieren, die eigentlich als Test-Stubs gedacht waren.
Mit PHPUnit 12, das im Februar 2025 veröffentlicht wurde, erreichte die Entwicklung der Test Doubles ihren vorläufigen Höhepunkt. In dieser Version wurde die Unterscheidung zwischen Test Stubs und Mock Objects endlich konsequent umgesetzt:
- Auf mit
createStub()erstellten Test Stubs können keine Erwartungen mehr konfiguriert werden - Mit
createMock()erstellte Mock Objects behalten natürlich die Funktionalität zur Konfiguration von Erwartungen
Die API-Trennung ist damit technisch vollständig umgesetzt.
Seit Version 12.5 gibt PHPUnit eine Meldung aus, wenn ein Test ein Mock Object erzeugt, auf dem jedoch keine Erwartungen konfiguriert wurden. In einem solchen Fall wurden die Erwartungen entweder vergessen und müssen ergänzt werden, oder der Testcode kann so geändert werden, dass statt eines Mock Objects ein Test Stub verwendet wird.
Entwicklerinnen und Entwickler können nun beim Lesen von Testcode sofort erkennen, welche Intention hinter der Verwendung eines Test Doubles steht. Ein Aufruf von createStub() signalisiert beispielsweise, dass eine Abhängigkeit entkoppelt wird, während createMock() darauf hinweist, dass die Kommunikation zwischen Objekten getestet wird.
Durch diese explizite Unterscheidung sind sie gezwungen, bewusst über die Rolle jedes Test Doubles nachzudenken. Dadurch werden die Tests besser strukturiert und wartbarer. Neue Entwicklerinnen und Entwickler, die gerade erst mit PHPUnit beginnen, können die verschiedenen Konzepte leichter verstehen und korrekt anwenden, da die API die richtige Verwendung erzwingt.
Die Weiterentwicklung der Test Doubles in PHPUnit hatte weitreichende Auswirkungen auf die PHP-Community. Viele Projekte mussten ihre Testsuite überarbeiten, um von den veralteten APIs zu den neuen, klareren Methoden zu migrieren. Zwar war diese Migration mit Aufwand verbunden, führte aber letztendlich zu besser strukturierten und verständlicheren Tests.
Die schrittweise Einführung der Änderungen über mehrere PHPUnit-Versionen hinweg zeigt, dass ich mir die entsprechenden Entscheidungen nicht leicht gemacht habe und bemüht war, eine für alle tragbare Lösung sowie einen gangbaren Weg dorthin zu finden. Anstatt drastische Breaking Changes einzuführen und mir damit viel Arbeit zu sparen, wurde ein Migrationspfad geschaffen, der es Teams ermöglicht, ihre Tests schrittweise zu modernisieren.
Die Entwicklung von Test Doubles in PHPUnit – vom anfänglichen getMock() hin zur klaren Trennung zwischen createStub() und createMock() – stellt einen bedeutenden Fortschritt in der PHPUnit-Entwicklung dar. Diese Veränderung geht über eine reine API-Verbesserung hinaus und spiegelt vielmehr eine Reifung im Verständnis von Testpraktiken und deren Umsetzung in Code wider.
Die konsequente Unterscheidung zwischen Test Stubs und Mock Objects verbessert nicht nur die Qualität der Tests, sondern erleichtert auch deren Lesbarkeit und Verständlichkeit. Die Intention hinter jedem Test Double ist nun klar erkennbar, was zu wartbareren, verständlicheren und robusteren Tests führt. Diese Entwicklung zeigt eindrucksvoll, wie sich ein Framework im Laufe der Zeit weiterentwickeln kann, um seinen Anwenderinnen und Anwendern nicht nur bessere Funktionen zu bieten, sondern sie auch zu besseren Praktiken anzuleiten.
Schlussgedanken
Jedes Framework und jedes Werkzeug spiegelt die Erfahrungen, das Wissen und die Vorlieben seines Entwicklers oder seiner Entwicklerin wider. Für mich ist eine klare Unterscheidung zwischen Test Stubs und Mock Objects wichtig. Ich ziehe Mock Objects gegenüber Test Spies vor, wahrscheinlich, weil ich sie zuerst kennengelernt habe. Wenn dir das Angebot von PHPUnit nicht zusagt, dann schau dir gerne Alternativen wie Mockery und Prophecy an.
Entscheidend ist, dass du Werkzeuge und Ansätze wählst, die deinen Testcode übersichtlich machen und deine Teststrategie für dein Team und dich selbst transparent gestalten. Damit schaffst du die Grundlage für Tests, die technisch einwandfrei, wartbar und robust sind. Dabei helfe ich dir gerne.