Gurken zum Kaffee

Cucumber-JVM: Yet another BDD-Framework für Java

Liviu Carausu

© Shutterstock / artem_ka

Die Grundlagen von Behavior-driven Development wurden bereits im Artikel „Verhaltensregeln für Anwendungen“ (Java Magazin 8.2014) erläutert. Dieser Beitrag ist eine Einführung in BDD mit Cucumber-JVM, einem BDD-Framework, das unter Java-Entwicklern immer beliebter wird. Mit Cucumber-JVM kann man lesbare und wartbare Akzeptanztests für Java-Anwendungen schreiben.

Cucumber ist ein Behavior-driven-Development-Werkzeug zur textbasierten Spezifikation von Anforderungen an Software und zur automatisierten Überprüfung dieser Beschreibung auf ihre korrekte Implementierung. Es wurde ursprünglich in der Programmiersprache Ruby für Ruby-Anwendungen geschrieben. Inzwischen unterstützt es aber auch andere Programmiersprachen wie Java und andere auf der Java Virtual Machine laufenden Programmiersprachen wie Clojure, Groovy, JRuby, Jython, Scala u. a.. Aslak Hellesøy ist der Erfinder von Cucumber. Im April 2008 startete er das Cucumber-Projekt, um die internen Designfehler und Usability-Probleme des RSpec Story Runner zu lösen und die Benutzerfreundlichkeit zu verbessern.

Hinter dem Namen „Cucumber“ verbirgt sich keine tiefere Bedeutung. Die Idee zu diesem „silly but catchy name“ hatte Hellesøys Frau [1]. Cucumber wurde entwickelt, um Brücken zwischen den Mitgliedern mit und ohne technischem Hintergrund eines Softwareteams aufzubauen. Für Java-Entwickler gab es am Anfang Cuke4Duke (2010), welches später (2012) in die Cucumber-JVM wanderte, weil „die Benutzung eine Qual war – schwer zu installieren und sowohl langsam als auch schwergängig“, wie Aslak selbst erklärte.

Features und Scenarios

Die Hauptartefakte von Cucumber werden Features genannt. Jedes Feature ist in einer Datei mit der Endung .feature enthalten. Diese werden in der Sprache Gherkin geschrieben, die eine „Business Readable, Domain-specific Language“ ist, womit man das Verhalten von Software beschreiben kann, ohne zu spezifizieren, wie dieses Verhalten implementiert ist.

Die Sprachvoreinstellung für Gherkin ist Englisch. Die Gherkin-Grammatik ist zum Zeitpunkt des Verfassens dieses Beitrags für 37 Sprachen definiert. Um die Sprache zu wechseln, reicht es aus, ein #language-Tag in einer .feature-Datei zu definieren, z. B. für Deutsch: #language: de. Ein Gherkin-Quelldatei sieht in der Regel so aus wie in Listing 1 dargestellt.

Listing 1: "feature"-Datei-Beispiel
#language:de
1: Funktionalität: Eine knappe Beschreibung von dem, was gewünscht wird
 2:   Textbeschreibung des Geschäftswerts dieser Funktion
 3:   Geschäftsregeln, die den Umfang der Funktion regeln
 4:   Zusätzliche Informationen, die helfen, die Funktion einfacher zu verstehen 
 5: 
 6:   Szenario: Eine bestimmte Geschäftssituation
 7:     Angenommen eine Voraussetzung
 8:       Und eine andere Voraussetzung
 9:      Wenn eine Aktion von dem Aktor
10:       Und eine andere Aktion
11:       Und eine andere Aktion
12:      Dann wird ein testbares Ergebnis erreicht
13:       Und etwas, was wir überprüfen können, geschieht auch
14: 
15:   Szenario: Eine andere Situation
16:       ...

In Tabelle 1 sind die in Gherkin vordefinierten Schlüsselwörter auf Englisch und Deutsch beschrieben.

Tabelle 1: In Gherkin vordefinierte Schlüsselwörter auf Englisch und Deutsch

Tabelle 1: In Gherkin vordefinierte Schlüsselwörter auf Englisch und Deutsch

Ein Feature enthält eine Beschreibung und ein oder mehrere Szenarien. Als Beschreibung können Sie frei Text schreiben, bis das erste Szenario mit Szenario und einer neuen Zeile beginnt. Jedes Szenario besteht aus einer Liste von Schritten (Steps), die mit einem der Schlüsselwörter Angenommen, Dann, Wenn, Aber, Und (oder einer lokalisierten Version dessen) begonnen werden müssen. Jeder Schritt ist ein Satz, der zum Given-When-Then-Testparadigma gehört:

  • Given – die Szenariovoraussetzung
  • When – das Verhalten, das eine Aktion im System ausführt
  • Then – verifiziert Ergebnisse, die aufgrund des angegebenen Verhaltens erwartet werden

Wie die Cucumber-JVM die Steps mit Step-Definitionen verlinkt, soll weiter unten ausgeführt werden.

Schnelleinstieg in Cucumber-JVM

Das Starten eines Cucumber-JVM-Projekts vereinfacht ein Skeleton für Cucumber-Java-Anwendungen. Schlicht git clone https://github.com/cucumber/cucumber-java-skeleton.git eingeben, und schon hat man ein laufendes Cucumber-JVM-Projekt. Mehrere Beispiele, die die Cucumber-JVM-Funktionalität erläutern, sind hier zu finden.

Für diesen Beitrag habe ich ausgehend von Cucumber „Skeleton“ die Funktionalität eines Fahrkartenautomaten spezifiziert und getestet. Die Cucumber-Features werden als automatisierte Akzeptanztests verwendet. Der größte Vorteil hierbei ist, dass die Akzeptanztests die gelieferten Funktionalitäten gleichzeitig prüfen und in Form von „Living Documentation“ dokumentieren.

Als IDE habe ich IntelliJ IDEA 13.1.5 verwendet. Cucumber-JVM wird von IntelliJ bereits ab der Version 12 nativ unterstützt. Für Eclipse-Fans gibt es natürlich auch ein Plug-in.

Cucumber-JVM-Unterstützung in IntelliJ

  • Syntax- und Fehler-Highlighting
  • Schnelle Lösung, um Step-Definitionen zu erstellen
  • Navigation zwischen Step-Definitionen und Steps
  • Möglichkeit, die Steps in Englisch oder in einer anderen Sprache zu spezifizieren
  • Run-/Debug-Konfiguration
  • Möglichkeit, alle Features in einer Feature-Datei (durch ein besonderes Symbol gekennzeichnet) oder in einem Verzeichnis laufen zu lassen

 

Listing 2: "fahrkartenAutomat.feature"
#language:de
@fahrkartenAutomatService
Funktionalität: Ein Fahrkartenautomat muss abhängig von den User Eingaben den richtigen Fahrkartenpreis ermitteln.
  …..
@zone1 @barzahlung
Szenario: Fahrkarten Bestellung Zone 1 Barzahlung
    Angenommen Zone 1 ist selektiert
    Und Normal Tarif ist selektiert
    Und Es wird mit Bargeld bezahlt
    Wenn Der Fahrkartenpreis berechnet wird
    Dann Der Fahrkartenpreis ist 2,6€

@zone1 @geldkartezahlung
Szenario: Fahrkarten Bestellung Zone 1 GeldKarte Zahlung
    Angenommen Zone 1 ist selektiert
    Und Normal Tarif ist selektiert
    Und Es wird mit GeldKarte bezahlt
    Wenn Den Fahrkartenpreis berechnet wird
    Dann Den Fahrkartenpreis ist 2,5€
…....

Der Code aus Listing 2 ist eine Cucumber-.feature-Datei, die die Funktionalität eines Fahrkartenautomaten schildert. Zur Laufzeit sucht die Cucumber Engine für jeden Step, also für jede Zeile, die mit einem der Stichwörter Aber, Angenommen, Dann, Gegebensei, Gegebenseien, Und, Wenn anfängt, eine passende Step-Definition-Methode, die in einem Java-Objekt implementiert werden muss. Die gefundene Step-Definition wird von Cucumber ausgeführt.

Was man in den Step-Definitionen schreibt, ist Cucumber egal. Man muss aber auf die logische Bedeutung der Step-Definitionen aufpassen, damit die Tests verständlich bleiben. Zum Beispiel prüft man normalerweise in den Step-Definitionen für die @Dann-Methoden die erwarteten Ergebnisse. Jede Step-Definition-Methode wird mit einem Gherkin-Stichwort annotiert (@Aber, @Angenommen, @Dann, @Gegebensei, @Gegebenseien, @Und, @Wenn). Jede Step-Definition-Methode hat einen regulären Ausdruck, der in der zugehörigen Annotation spezifiziert ist. Das Pattern wird benutzt, um die Step-Definition mit allen passenden Schritten zu verknüpfen.

Das IntelliJ-Plug-in markiert farblich die Steps, für die noch keine Step-Definition vorhanden ist und bietet die Möglichkeit, für die Implementierung der Step-Definitionen (Listing 3) eine neue Java-Klasse zu definieren oder eine vorhandene Klasse zu nutzen. Ein Vorschlag für den regulären Ausdruck, der verwendet wird, um die Steps mit den Step-Definitionen zu verknüpfen, wird vom IntelliJ-Plug-in automatisch generiert. Wenn keine passende Step-Definition zur Laufzeit gefunden wird, dann erstellt die Cucumber Engine einen Undefined-step-Fehler.

Listing 3: Implementierung der Steps
public class TicketMachineGlue {
  private TicketMachine ticketMachine;
  public TicketMachineGlue(TicketMachine ticketMachine) {
    this.ticketMachine =  ticketMachine;
  }
  @Angenommen("^Zone (\\d+) ist selektiert$")
  public void selectZone(int selectedZone) throws Throwable {
    ticketMachine.setSelectedZone(Zone.values()[selectedZone - 1]);
  }
  @Und("(.*) Tarif ist selektiert$")
  public void selectTarifType(String selectedTarif) throws Throwable {
    if(selectedTarif.equals(TARIF_NORMAL)) {
      ticketMachine.setRate(Rate.NORMAL);
    }
    else if(selectedTarif.equals(TARIF_REDUZIERT)) {
      ticketMachine.setRate(Rate.REDUCED);
    }
  }
  ...
@Wenn("Der Fahrkartenpreis berechnet wird$")
public void calculateTicketPrice(){
  ticketMachine.calculateTicketPrice();
}

@Dann("Der Fahrkartenpreis Preis ist (.*)€$")
public void checkCorrectTicketPrice(double ticketPrice) throws Throwable {
  assertThat(ticketPrice).isEqualTo(ticketMachine.getTicketPrice());
}

In IntelliJ ist es möglich, die .feature -Dateien direkt aus der IDE auszuführen. Es können entweder einzelne oder alle .feature-Dateien eines Verzeichnisses ausgeführt werden. Es gibt diverse Möglichkeiten, eine .feature-Datei auszuführen. Eine leere Klasse, die mit der JUnit-Annotation @RunWith versehen ist, kann direkt als JUnit-Test von jeder IDE ausgeführt werden (Listing 4).

Die .feature-Dateien, die ausgeführt werden müssen, können mit dem features-Attribut der @CucumberOptions-Annotation explizit angegeben werden. Wenn das features-Attribut nicht spezifiziert ist, werden alle .feature-Dateien, die auf der gleichen Package-Ebene wie der JUnit-Test oder in Subpackages gefunden worden sind, ausgeführt.

Cucumber sucht standardmäßig die Glue-Klassen (bei der die Step-Definitionen und die Hooks definiert sind) im gleichen Package oder einem Subpackage relativ zum JUnit-Klasse-Package. Wer die „Convention Over Configuration“ ignorieren will, hat das glue-Attribut des @CucumberOptions zur Verfügung, um gezielte glue-Packages zu spezifizieren.

Listing 4
@RunWith(Cucumber.class)
@CucumberOptions(features = {"src/test/resources/ticketmachine/service/fahrkartenAutomat.feature"},
glue = {"ticketmachine.service"})
public class RunServiceTicketMachineTests {}

Eine Cucumber-.feature-Datei kann auch mit dem Cucumber JVM Command Line Interface (CLI) Runner, der in der Klasse cucumber.api.cli.Main implementiert ist, ausgeführt werden.

Verschiedene Varianten, um Cucumber-Tests auszuführen sind im Cucumber-JVM-Skeleton-Projekt enthalten und in dem Fahrkartenautomatbeispiel. Mithilfe der Kommandozeile aus dem Projekthauptverzeichnis kann man die Tests starten – entweder mit Maven oder mit Ant.

Für Maven muss man mvn test eintippen. Aber Vorsicht: nur die *Test-Klassen, die mit @RunWith(Cucumber.class) annotiert sind, werden ausgeführt. Mit Ant wird der CLI Runner benutzt. Einmalig ant download ausführen, dann ant runcukes – und die Tests werden gestartet.

Mehr Step-Definitionen

Reguläre Ausdrücke (Capture Groups) werden verwendet, um die Methodenparameter der Step-Definitionen zu definieren. Zum Beispiel (Listing 3) wird für die Methode selectZone(int selectedZone) die Gruppe (\\d+) verwendet, um das Integer, das gleich nach Zone kommt, zu identifizieren und als Methodenparameter zu übergeben. (@Angenommen(„^Zone (\\d+) ist selektiert$“)).

Cucumber bietet auch eine Testinitialisierung mit Datentabellen. Letztere sind sinnvoll zur Spezifizierung größerer Datenmengen. Wenn wir unseren Fahrkartenautomat mit Daten initialisieren wollen, können wir eine Datentabelle spezifizieren, wie in Listing 5 dargestellt.

Cucumber bietet zudem eine automatische Umwandlung von Tabellenzeilen in Objekte, vorausgesetzt, die Objektattribute stimmen mit den Tabellenspalten überein. Beispielsweise wird in Listing 5 eine Liste von PriceRecord-Objekten von Cucumber erzeugt und als Parameter für die Methode setupTicketMachine(List<PriceRecord> priceRecords) gegeben. Das PriceRecord-Objekt hat als Attribute zone, tarif, bezahlungArt und preis, die exakt mit den Examples-Tabellenköpfen übereinstimmen.

Listing 5: Data Tables
Szenario: Fahrkartenautomat init
    Angenommen der Fahrkartenautomat ist mit Data initialisiert
      | zone    | tarif            | bezahlungArt    | preis |
      | 1    | Normal   | Bargeld      | 2,6 |
      | 1    | Normal   | GeldKarte    | 2,5 |
      | 2    | Normal   | Bargeld      | 5,2 |
      | 2    | Normal   | GeldKarte    | 5,0 |
      | 3    | Normal   | Bargeld      | 7,8 |
      | 3    | Normal   | GeldKarte    | 7,5 |
      | 4    | Normal   | Bargeld      | 10,4 |
      | 4    | Normal   | GeldKarte    | 10,0 |
    Dann ist der Fahrkartenautomat betriebsbereit
 …...
Step Definition: 

@Angenommen("^Der Fahrkartenautomat ist mit Data initialisiert$")
public void setupTicketMachine(List<PriceRecord> priceRecords) throws Throwable {
  for(PriceRecord priceRecord : priceRecords)
  {
    // for example store the records in a database
    …. 
  }
}

public class PriceRecord {

  private String zone;

  private String tarif;

  private String bezahlungArt;

  private String preis;
  …..
}

Mehrere Step-Definitionen-Beispiele und Parameterumwandlungen kann man hier ansehen.

Backgrounds

Oft wiederholen sich beim Schreiben mehrerer Szenarien in einer .feature-Datei die Testschritte. Diejenigen, die für alle Szenarien gleich sind, können in eine Testschritt-Grundlage (Background) gezogen werden. Ein Beispiel ist die Voraussetzung in Listing 2 Normal Tarif ist selektiert. Diese Voraussetzung kann in einen Grundlage-Schritt herausgezogen werden (Listing 6). Diese Grundlage-Schritte werden vor jedem Szenario ausgeführt. Das kann die Lesbarkeit erhöhen und reduziert die Anzahl der Testschritte in der .feature-Datei.

Listing 6: "fahrkartenAutomatTarifBackground.feature"
#language:de
...
Grundlage: Normal Tarif ist selektiert
  Angenommen Normal Tarif ist selektiert

Szenario: Fahrkarte Bestellung Zone 1 Barzahlung
  Angenommen Zone 1 ist selektiert
  Und Es wird mit Bargeld bezahlt
  Wenn Die Fahrkarte Preis berechnet wird
  Dann Die Fahrkarte Preis ist 2,6€

Szenario: Fahrkarte Bestellung Zone 1 GeldKarte Zahlung
  Angenommen Zone 1 ist selektiert
  Und Es wird mit GeldKarte bezahlt
  Wenn Die Fahrkarte Preis berechnet wird
  Dann Die Fahrkarte Preis ist 2,5€
...

Datengetriebene Tests

Cucumber-JVM kann auch für datengetriebene Tests verwendet werden. Man kann z. B. die Funktionalität von fahrkartenAutomat.feature, die in Listing 2 teilweise enthalten ist, in einem datengetriebenen Test konzentrieren, wie Listing 7 zeigt. Der Szenariogrundriss wird für jede Zeile der Beispiele-Tabelle ausgeführt. Jedes Szenario wird dadurch generiert, dass der in den spitzen Klammern definierte Parameter aus dem Szenariogrundriss durch die zugehörigen Werte der Beispiele-Tabelle ersetzt wird. Für jeden Szenariogrundrissparameter gibt es einen Tabellenkopf mit dem gleichen Namen.

Listing 7
#language:de
Funktionalität: Fahrkartenautomat Datengetriebene Test.
  Szenariogrundriss: Fahrkartenautomat Funktionalität testen
    Angenommen Zone <zone> ist selektiert
    Und <tarif> Tarif ist selektiert
    Und Es wird mit <bezahlungArt> bezahlt
    Wenn Der Fahrkartenpreis berechnet wird
    Dann Der Fahrkartenpreis ist <preis>€

  Beispiele:
  | zone | tarif    | bezahlungArt | preis |
  | 1    | Normal   | Bargeld      | 2,6 |
  | 1    | Normal   | GeldKarte    | 2,5 |
  | 2    | Normal   | Bargeld      | 5,2 |
  | 2    | Normal   | GeldKarte    | 5,0 |
  | 3    | Normal   | Bargeld      | 7,8 |
  | 3    | Normal   | GeldKarte    | 7,5 |
  | 4    | Normal   | Bargeld      | 10,4 |
  | 4    | Normal   | GeldKarte    | 10,0 |

Tags

Tags sind eine sehr gute Möglichkeit, Ihre Features und Szenarien zu organisieren. Sie sind vor allem bei der automatisierten Testdurchführung sehr hilfreich, wenn man Sets von Szenarien oder Features definieren kann, die durchgeführt werden. Zum Beispiel kann man die Szenarien nur für eine bestimmte Komponente oder für eine bestimmte Entwicklungsphase ausführen.

Ein Feature oder Szenario kann mehrere Tags enthalten. Die Tags müssen durch Leerzeichen separiert werden. Jedes Tag, das auf einem Feature besteht, wird per Szenario, Szenariogrundriss oder Beispiel vererbt. Mithilfe des Tags kann man ein bestimmtes Set von Szenarien oder Features zum Ausführen selektieren. Die Tags spielen auch eine wichtige Rolle bei der Verknüpfung zwischen Hooks und Szenarien.

Zum Beispiel kann man die Szenarien in Listing 2 mit Tags wie @zone1 annotieren, für die Szenarien, die sich auf die zone1 beziehen oder mit @barzahlung für die Szenarien, wobei die Bezahlung per Bargeld durchgeführt wird. Man kann beim Ausführen von den Tests ein oder mehrere Sets von getaggten Szenarios spezifizieren. Beim JUnit Runner werden die Tags mithilfe des tags-Attributs der @CucumberOptions-Annotation spezifiziert: @CucumberOptions(tags= {„@zone1“}). Mann kann auch logische Operatoren verwenden wie tags={„~@zone1“}. Beim CLI Runner können die Tags im cucumber.options spezifiziert werden: cucumber.api.cli.Main  –Dcucumber.options=“–tags @zone1″ myfeature.feature

Hooks

Die Hooks ermöglichen es uns, Aktionen an verschiedenen Punkten im Cucumber-Testzyklus durchzuführen. Szenarien-Hooks kann man mit @cucumber.annotation.Before und @cucumber.annotation.After-Annotationen definieren. Die Methoden, die mit @Before und @After annotiert worden sind, werden vor und nach jedem Szenario ausgeführt, egal, wo die Szenarios definiert sind. Das ist aber nicht immer sehr hilfreich. Dafür bietet Cucumber auch die Möglichkeit, die Hooks mit Tags zu versehen. Die Hooks, die mit Tags annotiert sind, werden nur vor und nach den Szenarien ausgeführt, die mit denselben Tags annotiert sind.

Listing 8: Hooks "TicketMachineGlue.java"
@Before(value="@fahrkartenAutomatService")
public void beforeScenario() {
  ticketMachine.startUserSession();
}
@After(value="@fahrkartenAutomatService",order = 1)
public void afterScenarioOrder1() {
  ticketMachine.endUserSession();
}
@After(value="@fahrkartenAutomatService",order = 2)
public void afterScenarioOrder2() {
}

Man kann sogar mithilfe des order-Attributs die Reihenfolge, in der die Hooks ausgeführt werden sollen, spezifizieren. Die Hooks mit den höheren Nummern werden zuerst ausgeführt. Die Voreinstellung für order ist 10000. Globale Hooks out of the box sind momentan in der Cucumber-JVM nicht vorgesehen. Man kann aber tricksen, indem man ein paar statische Variablen und Flags verwendet.

Dependency Injection

Alle Hooks und Step-Definitionen, die einen Default-Konstruktor haben, werden out of the box vom Cucumber-JVM-Core erzeugt. Man muss für Cucumber-JVM nicht unbedingt einen Dependency-Injection-(DI-)Container verwenden. Es ist allerdings sehr kompliziert, ohne einen DI-Container zu arbeiten, wenn die Szenarien Statusinformationen teilen müssen. In Listing 3 wird ticketMachine z. B. vom DI-Container initialisiert und dem TicketMachineGlue-Konstruktor übergeben.

Cucumber-JVM unterstützt bis jetzt sechs DI-Container – Spring, PicoContainer, Guice, Weld, OpenEJB, Needle. Welche DI-Container die Applikation verwendet, wird abhängig von den Klassenpfad-JARs entschieden (cucumber-picocontainer.jar, cucumber-guice.jar usw). Nur eine Cucumber-DI-JAR-Datei ist im Klassenpfad erlaubt, sonst können die Tests nicht gestartet werden.

Automatisierte Webbrowsertests

Cucumber-JVM ist für die automatisierten Webbrowsertests sehr gut geeignet. Man verwendet dafür den Browserautomatisierungstreiber Selenium. Um Selenium in einem Cucumber-JVM-Maven-basierten Projekt zu verwenden, braucht es eine Abhängigkeit zu selenium-java und dann, abhängig vom jeweiligen Browser, mit dem man die Applikation testen will, die entsprechende Maven-Abhängigkeit: selenium-firefox-driver, selenium-chrome-driver, wie in Listing 9 dargestellt. Sehr interessant finde ich den Treiber phantomjsdriver. Mithilfe des PhantomJS-Treibers können die Browsertests auch auf Continuous-Integration-Servern laufen. Sie sind headless, und die Test-Reports können trotzdem Screenshots enthalten.

Listing 9: Cucumber projekt Selenium dependencies
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>...</version>
</dependency>

<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-firefox-driver</artifactId>
  <version>...</version>
</dependency>
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-chrome-driver</artifactId>
  <version>...</version>
</dependency>
<dependency>
  <groupId>com.github.detro.ghostdriver</groupId>
  <artifactId>phantomjsdriver</artifactId>
  <version>1.1.0</version>
</dependency>

Getestet wird die Webversion des Fahrkartenautomaten. Um die Webapplikation zu starten, habe ich einen Kommandozeilen-HTTP-Server verwendet und folgendes Kommando aus dem Projekthauptverzeichnis ausgeführt: http-server  src/main/resources/web.

Abb. 1: Fahrkartenautomat-Webseite

Abb. 1: Fahrkartenautomat-Webseite

Nachdem die gewünschte Konfiguration ausgewählt ist (Zone, Bezahlung, Tarif) und „Fahrtkosten berechnen“ ausgelöst wird, sollte anschließend der Fahrkartenpreis angezeigt werden. Das Szenario lässt sich automatisch gut testen, indem man direkt die Webseitenelemente manipuliert und mithilfe des Selenium-Treibers überprüft. Die Hauptseite (Abb. 1) wird mithilfe des PageObject-Pattern in meinen Test integriert.

Das PageObject sieht in etwa so aus, wie in Listing 10 dargestellt. Die definierten Klassenattribute sind die HTML-Komponenten, die vom User verändert werden, und die Klassenmethoden sind die Aktionen, die auf der Seite ausgeführt werden können: selectZone/Tarif/Payment(), calculatePrice() und Query-Methoden: getCalculatedTicketPrice().

Listing 10: "TicketMachinePage.java"
public class TicketMachinePage {

  @FindBys({@FindBy(id="zoneSelector"),@FindBy(tagName="option")})
  List<WebElement> zoneSelector;
  ...  
  @FindBy(id="triggerCalculate")
  WebElement priceCalculatorTrigger;
  
  @FindBy(id="price")
  WebElement price;

  public void selectZone(int selectedZone) {
    clickSelectedOption(String.valueOf(selectedZone), zoneSelector);
  }
  ...
  public void calculatePrice(){
    priceCalculatorTrigger.click();
  }
  ...
  public String getCalculatedTicketPrice(){
    return price.getText();
  }
  ...
}

Über die Annotationen @FindBy und @FindBys werden die WebElements automatisch in das PageObjekt injiziert. WebElements können über Methoden wie click() gesteuert werden, deren Werte können gelesen und überprüft werden.

Continuous Integration und Reporting

Die Cucumber-JVM-Tests lassen sich hervorragend in Contiuous-Integration-Server integrieren. So gibt es für Jenkins ein Plug-in von Master Thought. Die generierten Cucumber-JVM-Reports sehen sehr nett aus (Abb. 2). Auch in Atlassian Bamboo lässt sich Cucumber-JVM gut integrieren.

Abb. 2: Master-Thought-Report für Cucumber-JVM-Tests

Abb. 2: Master-Thought-Report für Cucumber-JVM-Tests

Quo vadis, Cucumber?

Wer glaubt, dass Cucumber ein reines Testwerkzeug ist, liegt falsch. Wie Aslak Hellesøy es vortrefflich in seinem Blog beschreibt, ist Cucumber „das weltweit am meisten missverstandene Collaboration Tool“, weil viele Anwender vergessen, dass Cucumber Teil des BDD-Prozesses ist, der nach Gojko Adzic besser „Specification By Example“ genannt wird. Und der BDD-Prozess besteht – aus der Vogelperspektive gesehen – aus mindestens zwei Phasen:

  1. Specifications Workshops – Businessanalysten setzen sich mit Testern und Entwicklern zusammen und diskutieren die fachliche Funktionalität, die implementiert werden muss. Die drei oben genannten Rollen sind auch als „Three Amigos“ bekannt. Als Outcome dieser Phase werden die Cucumber-.feature-Dateien geschrieben.
  2. Outside-in Development – Die Programmierer fangen an, die Software TDD-ähnlich zu entwickeln: Sie kodieren ein bisschen, lassen die Cucumber-Features immer weiterlaufen, bis die Systemfunktionalität implementiert ist. Der Hauptunterschied zur TDD-Vorgehensweise ist, dass BDD auf einer höheren Abstraktionsebene operiert.

Mit dem Produkt Cucumber Pro Produkt (Abb. 3) möchte Hellesøy den kollaborativen Prozess noch stärker unterstützen. Dieses ist allerdings kein Open-Source-Projekt. Sein Ziel ist es, ein Colaboration-Tool für Cucumber zu bieten, das „so einfach wie ein Textverarbeitungsprogramm oder eine Social-Networking-Website zu verwenden ist“.

Man sollte immer bedenken, was Andrew Premdas, Cucumber „Early Adopter“ einmal gesagt hat: „Your cucumber features should drive your implementation, not reflect it“. Also nicht erst den Code und danach die Features schreiben, sondern umgekehrt.

Abb. 3: Cucumber Pro Real Time Collaborative Editor

Abb. 3: Cucumber Pro Real Time Collaborative Editor

Cucumber Pro arbeitet in der Cloud mit GitHub- oder BitBucket-Projekten. Mit dem „Real Time Collaborative Editor“ kann man zusammen im Team an Featuredefinitionen arbeiten. Wenn ein spezieller „Cucumber Pro“ Formatter in dem Projekt verwendet wird, werden die Testergebnisse direkt in die Cucumber Pro Cloud hochgeladen und dem Team zur Verfügung gestellt. Inwieweit sich Cucumber Pro als kommerzielles Projekt durchsetzen kann, ist eine andere Frage.

Aufmacherbild: Cucumbers in a box via Shutterstock / Urheberrecht: artem_ka

Verwandte Themen:

Geschrieben von
Liviu Carausu

Liviu Carausu ist schon seit 1998 von Java begeistert und arbeitet als Senior Developer bei der znt GmbH in Burghausen.

Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: