zurück zum Artikel

Gwen: BDD-Framework für lesbare und refaktorisierbare Tests in Java

Alexander Schwartz

Viele IDEs bieten gute Refactoring-Werkzeuge, jedoch nur wenige sind für Testfälle in der domänenspezifischen Sprache Gherkin ausgestattet. Beim Einsatz des Gwen-Frameworks sind Tests lesbarer Java-Code, wodurch sich die Refactoring-Tools zum Schreiben von Testfällen nutzen lassen.

Gwen: BDD-Framework für lesbare und refaktorisierbare Tests in Java

Viele IDEs bieten gute Refactoring-Werkzeuge, jedoch nur wenige sind für Testfälle in der domänenspezifischen Sprache Gherkin ausgestattet. Beim Einsatz des Gwen-Frameworks sind Tests lesbarer Java-Code, wodurch sich die Refactoring-Tools zum Schreiben von Testfällen nutzen lassen.

In der Regel werden Entwickler beim Schreiben von Quellcode durch ihre Entwicklungsumgebung (IDE) unterstützt: Während des Schreibens vervollständigt sie automatisch Methoden, zeigt deren Parameter an und bietet die Möglichkeit, per Tastenkürzel Methoden und Klassen innerhalb eines Projekts umzubenennen.

Um wartbare und langlebige Software zu schreiben, muss der Quellcode verständlich und erweiterbar sein. Wird das Programm Schritt für Schritt ausgebaut, ist der Quellcode bei Bedarf neu zu strukturieren, um verständlich und erweiterbar zu bleiben. Bei derartigen Neustrukturierungen, dem sogenannten Refactoring, werden beispielsweise Variablen umbenannt oder Quellcode in neue Methoden verschoben, um Wiederholungen zu vermeiden. Typsichere Sprachen wie Java vereinfachen den Prozess.

Die gleichen Anforderungen, die für den Quelltext gelten, sind auch für die Testfälle gültig. Sollte einer von ihnen nach einer Codeänderung nicht mehr laufen, muss für Fachabteilung und Entwickler einfach und zuverlässig erkennbar sein, von welchen Annahmen der Testfall ausgeht, was die fachliche Motivation ist und was der Test bezwecken möchte.

Um Sprachbarrieren zwischen Entwicklern und Fachexperten abzubauen, bieten sich Fälle an, die das Verhalten des Systems aus Nutzersicht beschreiben. Ein Vorreiter dieses Behaviour Driven Development [1] (BDD) ist das Cucumber Test-Framework [2]. In der Sprache Gherkin werden darin Features in Szenarien aufgeteilt. Letztere sind, einer Satzschablone gleich, in der immer identischen Struktur Given/When/Then beschrieben:

Feature: Basic Arithmetic

In Order to avoid stupid mistakes
As a cautious person
I use a calculator.

Scenario: simple addition
Given a calculator I just turned on
When I add 4
And I add 5
Then the result is 9

Für jedes Feature der Anwendung werden unterschiedliche Szenarien beschrieben. In jedem sind jeweils der Ausgangszustand aus Sicht eines Nutzers (Given), danach eine oder mehrere durchgeführte Aktionen (When) und schließlich die Nachbedingungen (Then) festgehalten. Diese Beschreibungen lassen sich je nach Entwicklungsvorgehen bereits vor der Umsetzung des Features als Akzeptanzkriterien durch die Fachabteilung formulieren. Die Struktur der Satzschablone stellt dabei sicher, dass Entwickler die Szenarien automatisieren können.

Die Features und Szenarien wertet das Cucumber Framework aus. Für jeden Schritt eines Szenarios müssen Entwickler eine Methode bereitstellen (Step Definitions), der die variablen Anteile des Schritts als Parameter übergeben werden. So können mehrere Szenarien die Methoden verwenden und unterschiedliche Werte übergeben:

public class CucumberSteps {
Calculator c;

@Given("^a calculator I just turned on$")
public void a_calculator_I_just_turned_on()
throws Throwable {
c = new Calculator();
}

@When("^I add (\\d+)$")
public void I_add(long number) throws Throwable {
c.add(number);
}

@Then("^the result is (\\d+)$")
public void the_result_is(long result)
throws Throwable {
assertThat(c.getState()).isEqualTo(result);
}
}

Moderne IDEs wie IntelliJ IDEA [3] zeigen durch Syntax-Highlighting fehlende Methoden an und ermöglichen das Springen vom Testschritt an die entsprechende Codestelle. Über Content Assist zeigen sie beim Schreiben von Code und Tests mögliche Vervollständigungen an, unterscheiden bei den Vorschlägen allerdings nicht zwischen Given, When und Then (Abb. 1). Insgesamt bleiben Content-Assist und Refactoring hinter den Möglichkeiten von Java-Code zurück. Ansonsten zeigt sich erst zur Laufzeit, ob reguläre Ausdrücke, Methoden-Annotationen und Features wie gewünscht zusammenspielen.

Content-Assist für Gherkin in IntelliJ (Abb. 1)

Content-Assist für Gherkin in IntelliJ (Abb. 1)

Die Entwickler des durch seinen Musikidentifikationsdienst bekannten Unternehmens Shazam Entertainment haben sich des Problems angenommen und das Micro-Framework Gwen [4] erstellt. Mit ihm lassen sich in Java Testfälle im Given/When/Then-Schema schreiben. Durch die Typsicherheit sind Content-Assist und Refactoring in jeder modernen IDE möglich. Gwen ergänzt andere Test-Frameworks und lässt sich einfach in bestehende Tests (zum Beispiel JUnit [5] und TestNG [6]) integrieren.

Im einfachsten Fall fügt man der Testklasse drei statische Imports Given/When/Then hinzu. Diesen Methoden ist zudem eine Referenz für das zu testende System zu übergeben. Ein JUnit-Test im BDD-Stil kann mit Gwen wie folgt aussehen (alle Quellen stehen im Übrigen auf GitHub [7] zur Verfügung):

import static com.shazam.gwen.Gwen.given;
import static com.shazam.gwen.Gwen.when;
import static com.shazam.gwen.Gwen.then;

/**
* In Order to avoid stupid mistakes
* As a cautious person
* I use a calculator.
*/
public class FeatureCalculator {

// The system under test (SUT)
private CalculatorSUT theCalculator =
new CalculatorSUT();

@Test
public void scenarioSimpleAddition() {
given(theCalculator).isTurnedOn();
when(theCalculator).adds(4).adds(5);
then(theCalculator).showsAsResult(9);
}

}

Vergleicht man das Format der Klasse mit dem ursprünglichen Feature im Gherkin-Format, erscheint die Klasse ähnlich lesbar. Bei der Formulierung der Schritte lassen sich Parameter nur am Ende als Methodenparameter übergeben. Um die Lesbarkeit zu erhalten, sollten Methoden idealerweise einen Parameter besitzen. Komplexe Übergabeparameter könnten Entwickler zum Beispiel via Builder-Pattern initialisieren.

Die drei statischen Methoden bauen darauf auf, dass die Klasse CalculatorSUT drei Methoden anbietet, die jeweils eine Instanz einer weiteren Klasse zurückliefern. Diese Klassen implementieren die Methoden, die für die Given/When/Then-Schritte des Szenarios vorgesehen sind. Eine Arranger-Klasse ist für das Bereitstellen aller Methoden rund um Given zuständig, die Actor-Klasse für die um When und eine Asserter-Klasse für die um Then:

import com.shazam.gwen.collaborators.Actor;
import com.shazam.gwen.collaborators.Arranger;
import com.shazam.gwen.collaborators.Asserter;
import com.shazam.gwen.gwt.Given;
import com.shazam.gwen.gwt.Then;
import com.shazam.gwen.gwt.When;

import static org.fest.assertions.Assertions.assertThat;

public class CalculatorSUT implements
Given<CalculatorSUT.CalculatorArranger>,
When<CalculatorSUT.CalculatorActor>,
Then<CalculatorSUT.CalculatorAsserter> {

private Calculator device;

private CalculatorArranger arranger;
private CalculatorActor actor;
private CalculatorAsserter asserter;

public CalculatorSUT() {
device = new Calculator();
arranger = new CalculatorArranger();
actor = new CalculatorActor();
asserter = new CalculatorAsserter();
}

public CalculatorArranger given() {
return arranger;
}

public CalculatorActor when() {
return actor;
}

public CalculatorAsserter then() {
return asserter;
}

public class CalculatorArranger implements Arranger {

public void isTurnedOn() {
device = new Calculator();
}
}

public class CalculatorActor implements Actor {

public CalculatorActor adds(long val) {
device.add(val);
return this;
}

public CalculatorActor multipliesBy(long val) {
device.multiplyBy(val);
return this;
}

public CalculatorActor powerBy(long val) {
device.power(val);
return this;
}

}

public class CalculatorAsserter implements Asserter {

public void showsAsResult(long val) {
assertThat(device.getState()).isEqualTo(val);
}

}
}

Durch das Aufteilen der Schritte in die unterschiedlichen Klassen kann die IDE die Methoden passend zur getesteten Systemkomponente und zur Art des Schritts zur Vervollständigung anzeigen (Abb. 2). Auch ein Springen vom Szenario in die Implementierung ist möglich. Im Gegensatz zu Gherkin lässt sich in Java eine Methode mit Refactoring umbenennen und von einer Methode in das Szenario zurückspringen.

Content-Assist für Java in IDEA IntelliJ (Abb. 2)

Content-Assist für Java in IDEA IntelliJ (Abb. 2)

Fügt man statt einer bestehenden eine neue Methode ein, zeigt der Compiler der IDE mit einer roten Markierung einen Fehler an. Über einen Hotkey generiert die IDE auf Wunsch einen leeren Methodenrumpf in der passenden Klasse (je nach Kontext im Arranger, Actor oder Asserter).

Die Testfälle in Gherkin-Syntax zu Beginn des Artikels sind nun lesbarem Java-Code gewichen. Durch zusätzliche runde und geschweifte Klammern sieht der Testfall nun etwas technischer aus, das bewährte Given/When/Then-Schema bleibt aber erhalten.

Der Vorteil von Gwen liegt zunächst bei den Entwicklern, da sie ihre gewohnte Java-Umgebung nicht verlassen müssen und eine gute Hilfestellung beim Schreiben von Testfällen bekommen. Werden die Testfälle mit der Anforderung spezifiziert, können die Entwickler sie in Java-Code mit den bewährten Werkzeugen zur Testautomatisierung implementieren. Die Unterstützung der IDE bietet beim Einsatz von Gwen nur die Methoden an, die zum Schritt (Given, When oder Then) und zum getestenen System passen. Effizientes Refactoring, um zum Beispiel Methoden umzubenennen, wird erst durch Java-Code möglich. Durch die Möglichkeit des Refactoring und die gute automatische Vervollständigung kann sich im Laufe der Zeit aus den Methodennamen Schritt für Schritt eine konsistente fachliche Sprache entwickeln.

Da im Gegensatz zu Cucumber mit Arranger, Actor und Asserter weitere strukturierende Elemente zum Einsatz kommen, sind mehr Klassen zu schreiben. Das relativiert sich allerdings in einer modernen IDE mit intelligenten Templates.

Mit Gwen ist es unwahrscheinlicher als bei Cucumber, dass die Fachseite den Code der Tests schreibt. Während bei dessen Verwendung ein nicht implementierter Schritt im Testergebnis als "undefined" gemeldet wird, sind die Methoden bei Gwen zumindest mit einem fail() umzusetzen.

Trotzdem kann die Fachseite wie gewohnt im Given/When/Then-Schema Testfälle zusammen mit den Anforderungen definieren und darauf vertrauen, diese Struktur bei den implementierten Testfällen wiederzufinden. Bei der Verfeinerung der Testfälle bietet sich ein gemeinsames Implementieren im Sinne des Pair Programming an.

Im zuvor bemühten Beispiel mit dem Taschenrechner lassen sich mehrere Aktionen hintereinander in einer Zeile via Method Chaining aufrufen, da die einzelnen Methoden die aktuelle Instanz zurückliefern.

Wenn in einer Anwendung zwischen Dialogen gewechselt wird, ist es sinnvoller, eine Referenz eines weiteren Teils des zu testenden Systems zurückzugeben. Im Verlauf kann die Testmethode diese mit Given/When/Then benutzen.

Kombiniert man Gwen mit Arquillian Graphene [8], einem Werkzeug zum Testen von Webseiten, sehen die Testfälle wie unten dargestellt aus. Das bewährte Konzept des Page Object Pattern [9] lässt sich hier mit BDD kombinieren.

@RunWith(Arquillian.class)
public class StartPageFeature {

@Drone
protected WebDriver browser;

@Page
private StartPage startPage;

@Test
public void szenarioSearchWithMatches() {
given(startPage).isOpenedInBrowser();
ResultPage resultPage =
when(startPage).searchesFor("bdd");
then(resultPage).showsAResultCountOf(3);
}

}

Die Möglichkeit, Gwen mit anderen Testframeworks zu kombinieren, besteht nur deshalb, weil es keinen eigenen Test-Runner mitbringt. Shazam setzt es beispielsweise mit Robotium (zukünftig Espresso) für Android-Testfälle ein (vgl. Beispiele von Shazam).

Damit unterscheidet sich Gwen grundsätzlich von allen anderen BDD-Testframeworks, die alle einen Runner enthalten.

In größeren Projekten werden die Arranger-, Actor- und Asserter-Klassen, die im weiter oben gezeigten längeren Beispiel innere Klassen sind, als eigenständige Klassen im gleichen Package abgelegt. Sind sie immer noch zu groß, lässt sich jeder Schritt in einer eigenen Klasse als Command implementieren. Gwen stellt hierfür die drei Interfaces Action, Arrangement und Assertion bereit. Ein ausführliches Beispiel [10] dazu findet sich ebenfalls bei Shazam.

Insgesamt besteht das Gwen aus nur einer Klasse mit statischen Methoden und neun Interfaces, die alle in diesem Artikel vorgestellt wurden.

Obwohl das Micro-Framework noch sehr jung ist, spricht viel für seinen Einsatz: Es ist als Quellcode verfügbar, hat keine weiteren Abhängigkeiten und verträgt sich gut mit einer vorhandenen Test-Infrastruktur. Da es vergleichsweise klein und in sich abgeschlossen ist, muss man sich mit Blick auf Weiterentwicklung und Fehlerbehebung nicht auf die ursprünglichen Autoren verlassen. Zudem kommt das Framework nur bei der Entwicklung und Ausführung der Tests zum Einsatz und stellt keinen Teil des Endprodukts dar. Es hat also keinerlei Einfluss auf das Laufzeitverhalten bestehender Anwendungen.

Gwen besticht auf den ersten Blick durch die bessere Unterstützung der Entwickler beim Schreiben und Refactoring ihres Test-Codes. Gleichzeitig senkt es die Hürde, BDD-Testfälle zu schreiben, da es sich mit bestehenden Frameworks kombinieren lässt.

Die Fachseite profitiert von wartbaren Testfällen, die sich bei Erweiterungen und Änderungen der Fachlichkeit anpassen lassen. Die Testfälle bleiben lesbar und eine Rückverfolgung der implementierten Funktionen zu den Testfällen und der dort beschriebenen fachlichen Anforderung ist leichter möglich. Mit der Zeit kann sich dank Refactoring und automatischer Vervollständigung eine gemeinsame Sprache entwickeln. Dem Einstieg in Behaviour-Driven Development steht damit nichts mehr im Wege.

Alexander Schwartz
ist Principal IT Consultant bei der msg systems ag [11] in Frankfurt. Im Laufe der Zeit arbeitete er mit unterschiedlichen Webtechniken. Er schätzt agile Projekte und automatisierte Tests.
(jul [12])


URL dieses Artikels:
https://www.heise.de/-2520872

Links in diesem Artikel:
[1] http://de.wikipedia.org/wiki/Behavior_Driven_Development
[2] http://cukes.info/
[3] https://www.jetbrains.com/idea/
[4] https://github.com/shazam/gwen
[5] http://junit.org/
[6] http://testng.org/
[7] https://github.com/ahus1/bdd-examples
[8] http://arquillian.org/modules/graphene-extension/
[9] https://docs.jboss.org/author/display/ARQGRA2/Page+Objects
[10] https://github.com/savvasdalkitsis/bdd-with-gwen
[11] http://www.msg-systems.com/
[12] mailto:jul@heise.de