Dieser Artikel basiert auf einem Vortrag, den ich heute auf der PHP UK Conference in London zum ersten Mal gehalten habe.
In einem vorherigen Artikel habe ich beschrieben, wie sich mein Verständnis von Software von der prozeduralen Programmierung über die objektorientierte Programmierung bis hin zum Domain-Driven Design (DDD) und Event Sourcing entwickelt hat. Die Erkenntnis, dass nicht alle Objekte gleich sind, war dabei wichtig. Der Schritt von "Wir speichern nur den aktuellen Zustand" zu "Wir protokollieren alle Ereignisse" war jedoch revolutionär.
Event Storming schafft ein gemeinsames Verständnis. DDD, CQRS und Event Sourcing sind mächtige Muster. Zusammen erschließen sie eine elegante Wahrheit: Wenn wir unsere Tests an der Sprache unserer Fachlichkeit ausrichten und unser Test Framework an unsere Architektur anpassen, wird das Testen nicht zur Last, sondern zur Brücke.
Von den Anforderungen über den Code bis zur Dokumentation schließt sich so der Kreis: Unsere Tests verifizieren, dass bei der Verarbeitung unserer Commands die richtigen Events emittiert werden, und sie generieren visuelle Dokumentation in der Notation von Event Storming.
Testen in einer ereignisbasierten Welt
Als ich begann, mit Event Sourcing zu arbeiten, stand ich vor einer fundamentalen Frage: Wie testen wir Software, die ihre Zustandsänderungen nicht überschreibt, sondern als unveränderliche Ereignisse speichert?
In traditionellen CRUD-Systemen implementieren wir Tests, die größer als Unit Tests sind, in der Regel wie folgt: Wir versetzen die Datenbank in einen definierten Zustand, führen eine Operation aus und prüfen anschließend den resultierenden Zustand. In event-gesteuerten Systemen müssen wir jedoch umdenken. Der Fokus liegt nicht mehr darauf, einen geänderten Zustand zu verifizieren, sondern darauf, dass die richtigen Events emittiert wurden.
Diese Perspektivverschiebung ist tiefgreifend. Statt zu fragen "Welcher Zustand ist jetzt in der Datenbank?", fragen wir "Welche Ereignisse wurden aufgezeichnet?". Oder anders formuliert: Wir testen nicht mehr das "Was", sondern das "Was ist passiert".
Fachlichkeit, Code, Tests
Erinnern wir uns an Event Storming, eine Methodik aus dem Collaborative Modeling. Anstatt allein im stillen Kämmerlein UML-Diagramme zu zeichnen, bringt Event Storming alle Beteiligten in einen Raum zusammen. Mithilfe unzähliger orangefarbener Sticky Notes modellieren wir gemeinsam den Zeitstrahl der Domäne anhand von Ereignissen.
Diese orangefarbenen Zettel, die sogenannten Domain Events, sind nicht nur wertvolle Kommunikationshilfen. Sie dienen auch als Blaupause für unsere Tests. Jedes im Workshop identifizierte Domain Event wird später zu einem Event in unserem Code. Mithilfe unserer Tests verifizieren wir, dass die richtigen Events unter den richtigen Bedingungen emittiert werden.
Die verschiedenfarbigen Sticky Notes des Event Stormings haben ihre Entsprechung im Testcode:
- Events nutzen wir sowohl als Testinventar ("Was ist passiert?") als auch für Zusicherungen ("Was soll passieren?")
- Commands sind, was wir unseren Tests testen ("Was soll ausgeführt werden?")
- Read Models testen wir, indem wir Projektionen basierend auf Events im Testinventar erzeugen
- Hotspots stehen für Probleme oder Fragen und können zu Tests für Edge Cases werden
Given, When, Then
Mithilfe des "Given, When, Then" Patterns können wir unsere Tests präzise und verständlich schreiben:
- Given – Vergangenheit: Ereignisse, die bereits stattgefunden haben
- When – Gegenwart: Command, das wir ausführen
- Then – Erwartung: Ereignisse, die wir erwarten, oder Exception, die ausgelöst werden soll
Dieses Beispiel zeigt, wie das Testen eventbasierter Systeme einfach und effektiv wird, wenn wir eine fachnahe Ausdrucksweise (Domain-Specific Language) verwenden können:
final class WithdrawMoneyCommandProcessorTest extends EventTestCase { #[TestDox('Emits a MoneyWithdrawnEvent when money is withdrawn')] public function testEmitsMoneyDepositedEvent(): void { $amount = Money::from(123, Currency::from('EUR')); $description = 'the-description'; $this->given( $this->accountOpened('the-owner'), ); $this->when( $this->withdrawMoney( $amount, $description, ), ); $this->then( $this->moneyWithdrawn($amount, $description), ); } }
Die Ausführung des oben gezeigten Tests ergibt die unten gezeigte Ausgabe im TestDox-Format:
DepositMoneyCommandProcessor ✔ Emits a MoneyDepositedEvent when money is deposited
Die projektspezifische Basisklasse EventTestCase stellt unter anderem die folgenden Methoden zur Verfügung:
-
given()konfiguriert die Events, die die Ausgangssituation für unseren Test definieren -
accountOpened()ist eine Hilfsmethode, die ein "Account Opened" Event erzeugt -
when()stößt die Command-Ausführung an, die wir testen wollen -
withdrawMoney()ist eine Hilfsmethode, die ein "Withdraw Money" Command erzeugt -
then()konfiguriert die Events, die wir als Ergebnis der Command-Ausführung erwarten -
moneyWithdrawnist eine Hilfsmethode, die ein "Money Withdrawn" Event erzeugt
EventTestCase verwaltet dabei hinter den Kulissen den Event Store und abstrahiert die Delegation an den für ein Command zuständigen Command Processor.
Über die reine Verifikation hinaus nutzt EventTestCase die gesammelten Informationen aus den drei Phasen (Given, When, Then), um mittels TestCase::provideAdditionalInformation() strukturierte Informationen über den ausgeführten Test an eine Extension für PHPUnit weiterzureichen, die aus diesen Daten visuelle Dokumentation in der Notation von Event Storming erzeugt:
Wenn wir diese strukturierten Informationen über alle Tests hinweg aggregieren, können wir alle Events, Command Processors und Read Models in einer Übersicht visualisieren:
Das vollständige Codebeispiel findest du auf GitHub. Die für diesen Artikel relevanten Codestellen sind im Material zu meinem Vortrag hervorgehoben.
Von der Last zur Brücke
Als ich anfing, mit Event Sourcing zu arbeiten, empfand ich es beim Testen zunächst als zusätzliche Komplexität. Wir müssen "in Events" denken, Command Handler verstehen, Projektionen testen und vieles mehr. Doch mit der Zeit erkannte ich, dass das Testen durch Event Sourcing einfacher, eleganter und aussagekräftiger wird.
Wenn ich heute auf meine Anfänge zurückblicke, von "Hello World" in AmigaBASIC bis hin zu Event Sourcing-Systemen mit automatisch generierter Dokumentation in Event Storming-Notation durch das Ausführen der Tests, dann erkenne ich eine klare Entwicklung. Diese betrifft nicht nur die Art und Weise, wie wir Systeme entwerfen und Code strukturieren, sondern auch unsere Denkweise in Bezug auf Tests.
Tests sind nicht mehr nur Verifikation. Sie sind Spezifikation, Dokumentation und Kommunikationswerkzeug in einem. Sie schließen den Kreis von den Ideen im Event Storming-Workshop bis zur kontinuierlichen Weiterentwicklung der Software.
Und genau das ist die elegante Wahrheit, die DDD, CQRS und Event Sourcing gemeinsam erschließen: Wenn wir unsere Tests an der Domänensprache ausrichten und unser Test Framework an unsere Architektur anpassen, wird das Testen zur Brücke statt zur Last. Eine Brücke, die unser Team zusammenhält, unsere Dokumentation lebendig macht und unsere Software verständlicher macht.