Über unsMediaKontaktImpressum
Sven-Torben Janus 25. Februar 2020

Event-Storming-Modelle testgetrieben implementieren

Domain-Driven Design (DDD) ist nach wie vor im Aufwärtstrend. Unter seinen vielen Methoden findet heute vor allem Event Storming weite Verbreitung. In Form eines Modells aus Post-its manifestiert sich dabei ein interdisziplinäres Verständnis des Problem- oder Lösungsraums an der Wand. In diesem Artikel beschreibe ich, wie dieses kollaborativ erarbeitete Modell testgetrieben mittels Patterns des Behaviour-Driven Developments (BDD) in Software überführt werden kann.

Anderthalb Jahrzehnte nach dem Erscheinen von Eric Evans "Big Blue Book" [1] hat die DDD-Community einen breiten Methodenkoffer geschaffen. Als ein Teil dieses Koffers erfreut sich vor allem Event Storming [2] immer größerer Beliebtheit und Verbreitung.

Die Workshop-Methode ermöglicht den beteiligten Personen – von Fachexperten zu Entwicklern –, ein interdisziplinäres Verständnis des fachlichen Problem- und Lösungsraums zu entwickeln. Es manifestiert sich in Form eines sogenannten Domänenmodells aus einer Menge von Post-its an der Wand. Je nach Ausrichtung und Ziel des Workshops kann es von grob granularen "Big-Picture"-Modellen bis hin zu detaillierten, implementierungsnahen Modellen variieren.

Insbesondere für die implementierungsnahen Modelle kommt dabei immer wieder die Frage auf, wie sich diese Modelle in ausführbaren Code überführen lassen. Mittels Test-Driven Development (TDD) [3] und insbesondere Behaviour-Driven Development (BDD) [4] lässt sich dies recht einfach bewerkstelligen. Wie das genau geht, möchte ich in diesem Artikel erläutern.

Dazu stelle ich zunächst die Bausteine des Event Stormings sowie Grundlagen des BDD anhand der Sprache Gherkin vor [5]. Anschließend identifiziere ich einige häufig anzutreffende Patterns im Event Storming und zeige, wie diese in Testspezifikationen auf Basis von Gherkin überführt werden können.

Event Storming

Die Modellierung im Event Storming startet mit der Identifikation von Schlüsselereignissen der Fachdomäne auf orangefarbigen Post-its. Diese sogenannten Domänenereignisse können alle Ereignisse sein, die im Rahmen des zu modellierenden Prozesses oder Geschäftvorfalls Relevanz besitzen. Die Ereignisse werden in zeitlicher Reihenfolge sortiert, so dass sie den zeitlichen Verlauf des Prozesses abbilden. Stehen die wesentlichen Ereignisse einmal fest, stellt sich die Frage, durch welche Aktionen die Ereignisse ausgelöst werden. Solche Aktionen werden im Event Storming durch sogenannte "Kommandos" oder "Befehle" auf blauen Post-its modelliert. Sie stellen in der Regel Entscheidungen von Benutzern dar und werden daher von diesen initiiert.

Damit ein Benutzer ein Kommando auslösen beziehungsweise seine Entscheidung treffen kann, werden Informationen aus der realen Welt und über den Zustand der Anwendung benötigt. Diese Informationen werden mittels grüner Post-its modelliert. Sie können aus der Menge aller bereits aufgetretenen Ereignisse abgeleitet werden. Aktionen können gegen externe Systeme ausgeführt werden, die in Form rosafarbener Post-its dargestellt werden. Dies geschieht entweder durch Benutzer oder alternativ durch eine Richtlinie (Policy). Richtlinien werden durch fliederfarbene Post-its modelliert. Sie sind reaktiv und reagieren auf eingetretene Ereignisse.

Ist der wesentliche Prozess einmal definiert, können vor allem die Entwickler in ein implementierungsnäheres Design einsteigen. Hierzu werden Aggregate in Form breiter gelber Post-its identifiziert, die neben externen Systemen die Verantwortlichkeit für die Verarbeitung von Kommandos übernehmen. Aggregate dienen dabei der Strukturierung der Anwendung und sind vor allem dafür verantwortlich, die Invarianten des Systems aufrecht zu erhalten.

Abb. 1 gibt einen Überblick über die Zusammenhänge.

Durch diese Art der Modellierung entstehen Prozess- oder Softwaremodelle, die gewissen Patterns folgen. Zur Verdeutlichung soll der nachfolgende Ausschnitt eines Online-Bestellprozesses dienen (s. Abb. 2).

In diesem Prozess hat ein Online-Kunde Produkte in seinen Warenkorb gelegt. Durch eine Richtlinie wurde festgehalten, dass wann immer der Warenwert größer als 100€ ist, die Versandkosten für die Bestellung erstattet werden sollen.

Der Online-Kunde bekommt die Waren in seinem Warenkorb, den Gesamtpreis und die Versandkosten angezeigt. Indem er den Warenkorb bestellt, löst er eine Bestellung aus. Hat er die Bezahlung per Rechnung gewählt, wird die Ware direkt verschickt und eine Rechnung erstellt. Die Rechnungsstellung erfolgt über ein externes Buchhaltungssystem. Zusätzlich erfolgt die Anmeldung der Waren zum Zoll durch die Internetzollanmeldung, wenn der Online-Kunde eine Lieferadresse außerhalb der EU angegeben hat.

Behaviour-Driven Development & Gherkin

Wie lässt sich ein solcher Prozess nun in Testfälle überführen. Eine Möglichkeit bietet das AAA-Pattern. AAA steht für Arrange, Act und Assert und dient der Einteilung von Testfällen in drei aufeinanderfolgende Phasen. Es kommt zum Beispiel in den Schlüsselworten Angenommen (Given), Wenn (When) und Dann (Then) der Sprache Gherkin zum Einsatz, die nachfolgend als Beispiel dienen soll, und folgt dem Schema:

Phase Semantik Beispiel
Arrange Angenommen (Given) das System befindet sich in einem definierten Zustand. Angenommen der Wert des Warenkorbs übersteigt 100 €.
Act Wenn (When) eine Aktion ausgeführt wird, Wenn der Kunde den Warenkorb bestellt,
Assert dann (Then) sollten sich beobachtbare Konsequenzen ergeben. dann fallen keine Versandkosten an.

Aufgrund der Natürlichsprachlichkeit eignet sich Gherkin hervorragend, um Testfälle aus Event-Storming-Modellen abzuleiten. Zum einen zahlt sie direkt auf die im DDD viel beschworene Ubiquitous Language[6] ein. Zum anderen ergeben sich aus den Phasen und den Schlüsselworten einfache Patterns zur Übersetzung zwischen Event-Storming-Modell und Testfallspezifikation.

Patterns im Event Storming

Auf Basis von Patterns lassen sich die einzelnen Teile sehr einfach herunter brechen.

Act / Wenn / When

Aktionen lassen sich in Event-Storming-Modellen immer durch Kommandos und damit durch blaue Post-its identifizieren, die vor einem Aggregat (gelbes Post-it) stehen.

Fragestellungen, die durch die Kommandos beantwortet werden, sind:

  • Was wird getestet?
  • Welche Eingabedaten werden getestet, wenn die Aktion ausgeführt wird?

Geht man davon aus, dass die Post-its in zeitlicher Reihenfolge von links nach rechts sortiert sind, finden sich die Eingabedaten auf grünen Post-its links neben den Kommandos. Für ein solches Kommando lässt sich daher immer wie folgt definieren:

    Wenn der Benutzer das Kommando ausführt,

oder im konkreten Beispiel:

    Wenn der Kunde den Warenkorb bestellt,

Arrange / Angenommen / Given

Für jedes Kommando muss aber zunächst in der Arrange-Phase der Kontext beschrieben werden, in dem die Aktion ausgeführt wird. Hierzu lassen sich folgende Fragen stellen:

  • Welche Ereignisse sind aufgetreten, bevor die Aktion ausgeführt wird?
  • In welchem Zustand befindet sich das System, bevor die Aktion ausgeführt wird?
  • Welche Vorbedingungen müssen erfüllt sein, bevor die Aktion ausgeführt wird?
  • Welche Daten sind im System?

In diesem Fall ist ein zweiter Blick auf die Post-its links neben dem Kommando notwendig. Dort finden sich potentiell alle Ereignisse, die aufgetreten sind. Ein Blick rechts neben das blaue Post-it zeigt in Form eines Aggregats (gelbes Post-it) außerdem den notwendigen Zustand des Systems vor der Aktion. Unter der Prämisse, dass ein System mittels Event Sourcing implementiert wird, lassen sich der Zustand eines Aggregates sowie die Informationen auch immer aus den aufgetretenen Ereignissen herleiten.

Für die Vorbedingungen eines Kommandos lässt sich daher immer wie folgt definieren:

    Angenommen das Ereignis X ist aufgetreten, 
    und das Ereignis Y ist aufgetreten...

oder im konkreten Beispiel:

    Angenommen das Produkt X wurde mit einem Preis von 70 € in den Warenkorb gelegt,
    Und das Produkt Y wurde mit einem Preis von 30 € in den Warenkorb gelegt
...

Aggregate beschreiben außerdem die Invarianten des Systems und sollen diese aufrecht erhalten. Es sollten daher einzelne Testfälle für diese Invarianten definiert werden. Durch unterschiedliche Vorbedingungen entstehen so mehrere Testfälle zu einem modellierten Kommando.

Durch das Schlüsselwort Angenommen lassen sich aber auch Bedingungen für Richtlinien definieren. Richtlinien basieren immer auf eingetretenen Ereignissen (und damit implizit auf dem Zustand des Systems). Da Richtlinien auf Ereignisse reagieren, werden auch Ereignisse oftmals zu Aktionen innerhalb der Act-Phase.

    Angenommen der Warenkorb enthält Produkte im Wert von 100 €, 
    Wenn der Warenkorb bestellt wurde, 
    Dann werden keine Versandkosten erhoben.

    Angenommen der Warenkorb enthält Produkte im Wert unter 100 €, 
    Wenn der Warenkorb bestellt wurde,  
    Dann werden Versandkosten in Höhe von 7,90 € erhoben.

Assert / Dann / Then

Wie das Beispiel zeigt, lassen sich unterschiedliche Wirkungen einer Richtlinie in der Assert-Phase prüfen. Sie dient aber vor allem der Prüfung zu erwartender Wirkungen einer Entscheidung eines Benutzers. In dieser Phase stellen sich daher folgende Fragen:

  • Was ist das zu beobachtende Ergebnis in Reaktion auf die Aktion?
  • Welche Nachbedingung, Ausgabedaten oder Ereignisse kann der Benutzer beobachten?
  • Welche Aktionen werden in einem externen System ausgelöst?

Diese Fragen lassen sich ebenfalls durch einen Blick auf das Event-Storming-Modell beantworten. Dazu ist ein Blick rechts neben das Aggregat notwendig. Dort finden sich die beobachtbaren Ereignisse in Form von orangenen Post-its. Je nach Flughöhe des Tests (z. B. Unit-Tests vs Integrationstests) empfiehlt sich dabei eventuell auch, Ereignisse weiter rechts im Prozess zu betrachten. So sind bei Integrationstests beispielsweise Ereignisse, die direkt vor einer Benutzerentscheidung auftreten (blaues Post-it zusammen mit gelbem Benutzer-Postit), gute Kandidaten für Nachbedingungen.

Bei der Kommunikation mit externen Systemen kann allerdings auch ein Kommando (blaues Post-it) vor einem externen System (rosafarbenes Post-it) zur Nachbedingung werden. Das gilt zumindest immer dann, wenn diese externen Systeme "getriggert" werden müssen und nicht reaktiv sind. Sollte das System reaktiv sein, findet sich in der Praxis vor dem Kommando oftmals eine "wann-immer"-Richtlinie (Eine Richtlinie, die ohne weitere Bedingungen auslöst, wenn die Ereignisse eintreten, die in die Regel einfließen). In diesem Fall sind die Ereignisse, die diese Richtlinie auslösen, gute Kandidaten, um das beobachtbare Verhalten des Systems zu beschreiben.

Testen auf unterschiedlichen Abstraktionsebenen

Mit diesen Pattern lassen sich nun Testfallspezifikationen auf unterschiedlichen Abstraktionsebenen ableiten. Diese variieren von einfachen, kleinschrittigen Unit-Tests bis hin zu Integrationstests für ganze User Journeys. Um dies zu verdeutlichen, soll der bereits vorgestellte Prozess dienen.

Testen einfacher Aktionen auf Ebene von Unit-Tests

Für die Ebene der Unit-Tests hilft eine Betrachtung der einzelnen Kommandos innerhalb des Prozesses. Grundsätzlich kann einer von zwei Fällen auftreten. Im ersten Fall ist das Kommando durch einen Benutzer initiiert (gelbes Post-it mit Bentzernamen/Rolle).

Ein Beispiel hierzu ist das Kommando "Bestelle Warenkorb".

Für die Beschreibung des Testfalls genügt ein Blick links neben das blaue Post-it. Dort finden sich Ereignisse, die als Vorbedingung dienen. Durch sie lässt sich die Arrange-Phase beschreiben.

    Angenommen, es wurde mindestens ein Produkt in den Warenkorb gelegt.
    
Das Kommando in Form des blauen Post-its ergibt die Act-Phase

    Wenn der Online-Kunde den Warenkorb bestellt,

Das Ergebnis findet sich wiederum in Form orangener Post-its rechts vom Kommando (hinter dem Aggregat oder externen System).

    dann wird der Warenkorb bestellt worden sein.

Ein weiteres Beispiel für dieses Pattern kann das Kommando "Versende Ware" sein. Nach dem gleichen Schema wie zuvor ergibt sich ein Testfall wie folgt:

    Angenommen der Warenkorb wurde bestellt.
    Wenn die Ware versandt wird,
    Dann wird die Ware versandt worden sein.

Solche Testfälle erscheinen zunächst trivial. Dies ist vor allem aber dem Beispiel geschuldet, in dem nur der Gutfall beschrieben ist. Modelliert man weitere Alternativen aus, erlauben solche kleinschrittigen Testfälle vor allem das Testen einzelner Aggregate und deren Invarianten.  

Testen von Richtlinien auf Ebene von Unit-Tests

Im zweiten Fall wird das Kommando durch eine Richtline ausgelöst. Im Beispiel ist das der Fall, wenn der Warenwert des Warenkorbs 100 € überschreitet. Richtlinien sind reaktiv und werden daher durch Ereignisse links von der Richtlinie getriggert. Die Act-Phase sollte daher ein Ereignis beschreiben. Wird die Richtlinie durch mehrere Ereignisse ausgelöst, müssen wir uns ein Ereignis als Trigger aussuchen. Die Arrange-Phase kann dann die weiteren Ereignisse beinhalten, um die übrigen notwendigen Vorbedingungen für die Richtlinie sicherzustellen. Das Ereignis selbst wird damit zur Nachbedingung in der Assert-Phase.

Am konkreten Beispiel sieht dies für die Erstattung von Versandkosten wie in Abb. 4 aus:

    Angenommen das Produkt X kostet 100 €.
    Und das Produkt Y kostet 0,01 €.
    Und das Produkt X wurde in den Warenkorb gelegt.
    Wenn das Produkt Y in den Warenkorb gelegt wurde,
    dann werden die Versandkosten erstattet werden.

Außerdem kann es natürlich vorkommen, dass mehrere Nachbedingungen zutreffen. Das sieht man z. B. im Falle der Bezahlung per Rechnung in Abb. 5.

    Angenommen der Warenkorb wurde bestellt.
    Wenn die Bezahlung per Rechnung gewählt wurde,
    dann wird die Ware versendet werden
    und die Rechnung wird in der Buchhaltung gestellt werden.

Puristen führen an dieser Stelle in der Regel an, dass Resultate immer in Form von Ereignissen auftreten und nur auf solche getestet werden sollten. Das ist aber für Richtlinien nicht immer möglich, wenn man diese als zu testende Einheit betrachtet. Vielmehr wäre es notwendig, den Prozessverlauf nach der Richtlinie weiter zu verfolgen, bis entweder ein Ereignis oder eine Systemgrenze in Form eines externen Systems auftritt. In diesem Fall verwischt aber die Grenze hin zu Integrationstests, da oftmals die Richtlinie zusammen mit ein oder mehreren Aggregaten getestet werden müsste. Ein Beispiel dazu könnte wie in Abb. 6 aussehen.

    Angenommen die Bezahlung per Rechnung wurde gewählt.
    Wenn der Warenkorb bestellt wurde,
    dann wird die Ware versandt worden sein.

Testen von größeren Zusammenhängen

Mit Blick auf Integrationstest sind vor allem die Interaktionsgrenzen des Systems zu beachten. Hierzu zählen externe Systeme, vor allem aber Benutzerinteraktionen mit dem System. Für Integrationstests empfiehlt es sich daher, zunächst Kommandos mit Benutzerinteraktion zu betrachten. Wie zuvor muss das Kommando "Bestelle Warenkorb" als Beispiel dienen. Dieses Kommando wird in der Act-Phase als zu testende Aktion definiert. Anschließend kann einem Pfad durch den Prozess gefolgt werden, der getestet werden soll. Alle notwendigen Vorbedingungen für Richtlinien und Aggregate entlang dieses Pfades werden dabei aufgesammelt und der Arrange-Phase formuliert. Der Pfad endet an einem (oder mehreren) Ereignisses vor einer Richtlinie, gefolgt von einem externen System (Systemgrenze) oder einer erneuten Benutzerinteraktion. Am konkreten Beispiel ergibt sich daraus folgende Testfallspezifikation (s. Abb. 7).

    Angenommen es wurden Produkte in den Warenkorb gelegt
    und der Online-Kunde hat standardmäßig Bezahlung per Rechnung gewählt.
    Wenn der Online-Kunde den Warenkorb bestellt,
    dann wird die Ware versandt worden sein.

    
Ereignisse als Trigger von Richtlinien werden dabei immer zu Vorbedingungen, sofern diese nicht aus dem zu testenden Pfad selbst ausgelöst werden (z. B. "Bezahlung per Rechnung ausgewählt").

Testen von Systemschnittstellen

Bei der Formulierung von Testfällen an Systemgrenzen ist es außerdem sinnvoll, die Grenze des Systems explizit zu machen. So könnte im Rahmen der Zollanmeldung die Grenze des modellierten Systems zum einen das Ereignis "Lieferadresse hinterlegt" sein. In diesem Fall wäre davon auszugehen, dass die Zollanmeldung reaktiv ist und mindestens die Richtlinie "Versand außerhalb EU" außerhalb des zu modellierenden Systems liegt. Alternativ wäre auch denkbar, dass die Richtlinie noch Teil des zu modellierenden Systems ist. In diesem Fall wäre tatsächlich das Kommando "Melde Ware zum Zoll an" die Systemschnittstelle.

Die Überführung des Modells in Testfälle erfolgt in beiden Fällen analog zu der Betrachtung für Unit-Tests.

Zusammenfassung

Es zeigt sich, wie einfach sich Event-Storming-Modelle mittels BDD-Patterns und ein paar Heuristiken in Testfallspezifikationen überführen lassen. Das gilt für kleinschrittige Unit-Tests wie auch für Integrationstest, die ganze User Journeys testen.

Ist eine solche Überführung nicht einfach möglich, zeigt meine Erfahrung, dass entweder der Prozess nicht vollständig modelliert wurde oder dieser in sich nicht schlüssig bzw. konsistent ist. Die Ableitung von Testfällen bietet daher nicht nur eine Möglichkeit, eben diese zur Prüfung der zu entwickelnden Software heranzuziehen, sondern vielmehr auch einen Mechanismus, das Modell selbst auf Korrektheit oder zumindest Konsistenz zu prüfen.

Zusätzlich sorgt die 1:1-Überführung des Modells in Testfälle dafür, dass eine einheitliche Sprache vom Modell weiter in Richtung der Implementierung getrieben wird. Die Patterns und Tests tragen daher maßgeblich zu einer Ubiquitous Language bei.  

Quellen
  1. E. J. Evans; 2003: Domain-Driven Design: Tackling Complexity in the Heart of Software
  2. Event Storming
  3. Wikipedia: Testgetriebene Entwicklung (TDD)
  4. Wikipedia: Verhaltensgetriebene Entwicklung (BDD)
  5. Gherkin
  6. UbiLang

Autor

Sven-Torben Janus

Sven-Torben ist Partner der Conciso GmbH, wo er als praktizierender Softwarearchitekt arbeitet. Er befürwortet er einen agilen und praktikablen Entwurf von Softwarearchitekturen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben