BDD UI Tests mit Serenity und JBehave

In meinem aktuellen Projekt schreiben wir die UI Tests als BDD Tests, da diese eher dazu führen, dass man fachliche Tests schreibt. Dazu verwenden wir Serenity zusammen mit JBehave.  Serenity verwendet dabei unter der Haube Selenium für die Browserautomatisierung. Dabei haben wir in meinem Team einen längeren Leidensweg hinter uns da zwischenzeitlich die Tests so schwer zu warten waren, dass wir mit dem Gedanken gespielt haben, die ganzen BDD /JBehave Tests wegzuschmeißen und durch reine Java Tests zu ersetzen. Letztendlich sind wir aber nach langer Zeit (dank Beratung eines anderen Teams) nun endlich bei einer Lösung angekommen, wo alles zusammenpasst und wir nun sehr zufrieden mit unseren UI Tests sind. Darum soll es in diesem Artikel gehen. 

Das Grauen am Anfang:

So sahen unsere JBehave Tests am Anfang aus:

Das Problem an obiger Testdefinition ist, dass schwer zu verstehen ist, was für diesen Test wirklich wichtig ist. Es gibt immerhin Tests, die die meisten Sätze teilen und somit fast identisch sind:

Um herauszufinden was die beiden Tests unterscheidet muss man schon ziemlich suchen.

Weiter muss man für jeden dieser Sätze wie bspw. the user provides valid payment data JBehave mitteilen, was es tun soll wenn dieser Satz auftaucht. Dieses tut man über Java Methoden die mit speziellen JBehave Annotationen (@When, @Given, @Then usw.) versehen sind:

Da wir nun sehr viele Sätze brauchen, um sein ein Szenario wie bspw. a self registered user is able to book zu beschreiben, brauchen wir auch sehr viele verstreute Java Methoden (so genannte Step Definitionen) um zu definieren was getan werden muss. Das Problem dabei ist, dass diese Methoden teilweise aufeinander aufbauen. So funktioniert der Schritt the user provides valid payment data nur, wenn man auch auf der richtigen Seite ist. Das ist aber schwer zu handhaben, wenn diese Java Step Definitionen so verstreut sind. Auch muss eine Java Step Definition teilweise in der globale Variable Daten ablegen, damit ein spätere Java Step Definition darauf zugreifen kann,  bspw. um über Assertions Werte zu prüfen oder auch um  weitere Daten einzugeben.

Auch sieht man im obigen Code Beispiel, dass wir versucht haben mehrere Steps über die @Composit Annotation zusammenzufassen. Das verwirrt noch zusätzlich. Will man rausfinden was bei dem Step an user was able to open the booking data page for an bookable door to door booking passiert kann man, da es noch in der JBehave Datei steht, direkt auf den Satz klicken und springt zu der zugehörigen Java Definition:

Dieses ist nun so ein @Composit Step, d.h. an sieht noch nicht was eigentlich passiert. Das aktuelle IntelliJBehave Plugin für  IntelliJ (Version 1.2) bietet keine Unterstützung davon, um nun zu den nächsten Step Definition zu springen. Das einzige was bleibt ist also Java Dateien nach  an user was able to open the booking data page for an bookable door to door booking oder the booking payment page is requested on the UI zu durchsuchen. Macht man das kommt man zur nächsten Java Methode:

Die wieder aus Composit Steps besteht nach denen man wieder sucht. Es ist also ziemlich mühsam, zu den Stellen zu navigieren, wo auch mal etwas passiert. Verbunden damit dass die Methoden nur funktionieren, wenn bestimmte Vorbedingungen erfüllt sind und sie sich globale Zustand teilen handelt es sich bei der jetzigen Lösung um Parade Beispiel für schwer wartbaren Spaghetticode.

Nach dem obigen Muster hatten wir mehrere Tests und wir merkten, dass sie immer unwartbarer wurden. Glücklicherweise hatte ein anderes Team sich schon sehr mit dem Schreiben von solchen Tests beschäftigt und half uns im Rahmen einer Mob Programming Session auszuarbeiten, wie wir von nun an unsere Tests schreiben wollen. Diese neue Lösung möchte ich im folgenden vorstellen.

Neue Lösung:

Die neuen JBehave Stories sehen wie folgt aus:

Verglichen mit vorher ist es nun deutlich kürzer. Man sieht nun auch schnell, was der Unterschied zu anderen Tests ist:

Grundsätzlich darf im When und Then Teil nun nur noch ein Satz stehen. Im Given Teil sollen bevorzugt ein Satz bis maximal zwei Sätze stehen. Das führt zu kurzen und prägnanten Testbeschreibungen die klar machen sollen, was getestet wird. Im Given Teil darf nur ein Model gefüllt werden. Es dürfen keine Aktionen ausgeführt werden, bspw. mit dem Browser. Stattdessen werden alle Aktionen im When Teil ausgeführt.

Im Java Code sieht das dann so aus:

Die beiden mit @Given versehenen Methoden führen noch keine Aktionen gegen den Server aus sondern befüllen nur ein internes Model im so genannten Session Storage das für das Szenario gilt. Würde einer der Given Steps schon Aktionen gegenüber den Server ausführen wie bspw. eine Buchung anzulegen, sich einloggen o.ä. dann müsste man wissen, welche Steps Aktionen ausführen und welche nur ein internes Model befüllen. Auch sollte bspw. der Step any valid booking  noch gar keine Buchung anlegen, da in anderen Szenarien Given Steps verwendet werden, die die Daten noch verändern und bspw. Pflichtdaten aus der Buchung wieder löschen um zu testen, dass man mit diesen unvollständigen Daten nicht die Seite verlassen kann:

Der dazugehörige Java Code, der Pflichtdaten auf dem Model löscht, sieht wie folgt aus:

Das ist alles kein Problem, da alle Aktionen mit dem Browser usw. nur im When Teil durchgeführt werden. Auch hat das Ausführen aller Aktionen im When Teil den Vorteil das man deutlich sieht, was passiert, da alles in einer Methode ausgeführt wird:

Dieses hat auch den Vorteil, dass man die Tests besser debuggen kann sowie gute IDE Unterstützung für die Navigation hat.

Im obigen When sind alle Schritte für den Test wichtig da hier der komplette Flow durchlaufen werden soll. Testen wir bspw. eine bestimmte Seite, wie bspw. die Payment Seite, ist das navigieren zu der Payment Seite für den Test nicht so relevant – der eigentliche Test geht erst los wenn wir auf der Seite sind. Scheitert schon einer der vorherigen Steps beim navigieren auf die Seite, wollen wir schnell erkennen können, das der Fehler wahrscheinlich woanders liegt. Beispielsweise liegt wahrscheinlich kein Problem im Payment vor wenn der Login nicht geht. In solchen Tests unterscheiden wir die Vorbedingung vom eigentlichen Test dadurch, dass wie die Vorbedingung in eine Methode precondition auslagern:

Die JBehave Step Definitionen alleine reichen nicht aus. Grundsätzlich verwenden wir drei Arten von Klassen:

Die JBehave Steps definieren den Java Code zu den Sätzen (wie Given any valid booking) in den JBehave Story Dateien. Sie verwenden die Serenity Page Steps (nicht zu verwechseln mit JBehave Steps) um die Seiten zu bedienen. Diese kapseln die fachlichen Dinge, die der Benutzer mit der Seite machen kann. So kann so eine Klasse bspw. aussehen:

Die @Step Annotation bewirken, dass die Aufrufe dieser Methoden inkl. Screenshot in dem Serenity Report auftauchen. D.h. man sieht nicht nur die fachlichen Schritte wie Given any valid booking sondern auch die tatsächlichen Aktionen die der Benutzer auf der Seite ausgeführt hat (wie bspw. providesBookingData):

Die Serenity Page Steps verwenden wiederum die Serenity Page Objekte:

Diese Aufteilung der Serenity Klassen in zwei Arten (Page Objekte und Page Steps) hat den Grund, dass die Serenity Page Objekte von Haus aus schon viele Low Level Methoden anbieten mit denen man bspw. Html Elemente suchen kann. Wir wollen aber nicht, dass man beim Schreiben von Tests auf diese Methoden außerhalb der Page Objekte zurückgreift und wir wollen dass man sofort sieht, welche Aktionen wie bspw. continueToReviewPage() auf der Seite möglich sind. Diese Aktionen die eine Seite anbietet, werden von der Serenity Page Step Klasse für eine bestimmte Seite angeboten. Dort gehen sie dann auch nicht in der Masse der Methoden unter, die Serenity von Haus aus für Page Objekte anbietet.

Fazit:

Wir haben nun schon viele Tests nach diesem neuen Prinzip geschrieben und alle alten Tests migriert. Die Tests sind nun deutlich klarer, mehr fachlich und besser zu warten. Auch die Benutzung von Serenity und JBehave fühlt sich nun gut an, vorher war es ziemlich mühsam.