Cucumber: Setup und Grundlagen

Keine Kommentare

Es gibt viele gute Akzeptanz-Test-Werkzeuge und BDD-Frameworks – und hier im codecentric-Blog wurde auch schon über eine ganze Reihe davon geschrieben, z. B. über das Robot Framework, Fitnesse, JBehave, Geb und andere. Ein wichtiges und beliebtes Werkzeug in diesem Bereich wurde bisher allerdings sträflich vernachlässigt: Cucumber. Es ist an der Zeit, diese Lücke zu schließen. Dieser Beitrag ist eine Einführung in Cucumber, ein BDD-Framework mit dem sich (unter anderem) sehr lesbare, wartbare und elegante Akzeptanz-Tests für Web-Anwendungen schreiben lassen.

Dies ist der erste Teil eines zweiteiligen Tutorials zu Cucumber. In diesem Teil geht es vor allem um das Setup, des Weiteren werden die Grundlagen von Cucumber und Gherkin erklärt. Wer bereits mit Cucumber vertraut ist, kann auch direkt zum zweiten Teil springen, in dem gezeigt wird, wie man mit Cucumber, Capybara, Poltergeist und PhantomJS Akzeptanztests für Web-Anwendungen schreibt.

Cucumber hat einen recht interessanten Stammbaum: JBehave wurde als RBehave auf Ruby portiert, RBehave wiederum wurde neu implementiert und in RSpec umbenannt. Zu RSpec wurde der RSpec Story Runner hinzugefügt, aus dem sich Cucumber entwickelt hat. Historisch betrachtet stammt Cucumber also entfernt von JBehave ab. Cucumber (ursprünglich in Ruby implementiert) ist später noch mit Cucumber JVM von Ruby zurück auf die JVM portiert worden, womit sich der Kreis schließt. Wenn man gerade auf der Suche nach einem JVM-basierten BDD-Framework ist, sollte man sich sowohl JBehave als auch Cucumber JVM anschauen. (Auf Stackoverflow gibt es einen knappen und etwas oberflächlichen Vergleich.)

Cucumber wurde außerdem noch auf ein Vielzahl anderer Plattformen portiert. Heutzutage lässt sich Cucumber mit Ruby (sozusagen das Original), Java, .NET, Adobe Flex, Python, Perl, Erlang und PHP benutzen. Man könnte sogar die selben Gherkin-Features (siehe unten) auf verschiedenden Plattformen ausführen, allerdings ist das eher eine theoretische Möglichkeit, denn die Step-Implementierungen (dazu kommen wir noch) sind jeweils Plattform-spezifisch.

Wie die meisten BDD-Frameworks wird Cucumber selten alleine benutzt. Insbesondere wenn man mit dem Werkzeug ernsthaft Web-Applikationen testen möchte, benötigt man einen kompletten Stack, der (mindestens) noch einen Browser-Treiber und eben einen Browser umfasst. In den meisten Fällen wird man den Browser-Treiber auch nicht direkt verwenden wollen, sondern eine Abstraktions-Schicht, die auf einem (oder verschiedenen) Browser-Treibern aufbaut – dies macht den Test-Code sehr viel lesbarer und wartbarer.

Selbst für erfahrene Akzeptanztester sind die Vielzahlt der möglichen Kombinationen in diesem Bereich manchmal etwas unübersichtlich. Allein auf der Ruby-Plattform könnte man unter anderem folgende Bibliotheken mit Cucumber kombinieren:

  • Selenium WebDriver (mit dem selenium-webdriver gem),
  • Watir WebDriver (baut auf Selenium WebDriver auf),
  • Capybara und Webrat,
  • Capybara und Selenium WebDriver oder
  • Capybara und Poltergeist (ein Treiber für PhantomJS)

…und selbst das ist nur eine Auswahl. Zusätzlich können einige der genannten Treiber (z. B. Selenium) verschiedene Browser steuern, so dass man sich noch entscheiden muss, ob man headless testen möchte (etwa mit PhantomJS oder Steam) oder ob man einen oder mehrere „echte“ Browser wie Firefox, Chrome oder IE zum Testen verwenden möchte. Fazit: Wenn man Cucumber für ein Projekt evaluiert, muss man letzten Endes auch einen (oder sogar mehrere) komplette Stacks evaluieren, was die Auswahl nicht unbedingt vereinfacht.

Dieses Tutorial verwendet Cucumber auf Ruby und im zweiten Teil zusätzlich noch Capybara, Poltergeist und PhantomJS. Meiner Erfahrung nach funktioniert diese Kombination sehr gut, besonders mit Seiten die viel mit Ajax arbeiten.

Das Tutorial ist eher für Leser geschrieben, die bisher noch keine oder nur wenig Erfahrung mit Ruby haben. Der erste Teil beschreibt die Installation und Konfiguration von Cucumber und wie man sein erstes Cucumber Feature schreibt. Der zweite Teil zeigt dann, wie man Cucumber und Capybara benutzt, um elegante Akzeptanztests für Web-Anwendungen zu schreiben.

Ruby, Cucumber und PhantomJS einrichten

Zuerst sollte das Beispiel-Projekt geklont werden, welches unter anderem den Gemfile enthält, der für das Einrichten von Cucumber benötigt wird:

git clone -b 00_setup https://github.com/basti1302/audiobook-collection-manager-acceptance.git

Um Cucumber und PhantomJS auf einem System einzurichten, kann eine der folgenden Gists benutzt werden. PhantomJS wird erst im zweiten Teil dieses Tutorial benötigt, kann also auch erst später installiert werden.

Wenn das Setup komplett ist, kann die Cucumber-Installation getestet werden, indem man im Hauptverzeichnis des Beispiel-Projekts cucumber eingibt. Das sollte die folgende Ausgabe erzeugen:

0 scenarios
0 steps
0m0.000s

Hinweis: Da wir den Ruby Bundler benutzen, sollten wir eigentlich bundle exec cucumber als Kommando benutzen (statt cucumber). Dies stellt sicher, dass das cucumber Kommando im Kontext des im Gemfile definierten Bundles ausgeführt wird. Solange nicht verschiedene Versionen eines Ruby Gems installiert sind, sollte das keinen Unterschied machen. Wenn Cucumber sich jedoch unerwartet verhält, sollte man es mit bundle exec cucumber versuchen – oder man gewöhnt sich einfach von Anfang an daran, immer bundle exec cucumber zu verwenden.

Cucumber Grundlagen

Features und Scenarios

Die Hauptartefakte von Cucumber werden Features genannt. Sie werden in einer DSL namens Gherkin geschrieben, die mehr oder weniger an natürlich Sprache angelehnt ist (wie natürlichsprachlich ein Feature daher kommt, hängt natürlich wesentlich vom Autor des Features ab.)

Die Default-Sprache für Cucumber/Gherkin ist natürlich Englisch, bei Bedarf können auch andere Sprachen benutzt werden. Dazu reicht es aus, dem Feature ein Language-Tag voranzustellen, etwa # language: de für Deutsch. Dieses Tutorial macht von dieser Möglichkeit allerdings keinen Gebrauch sondern arbeitet mit englischsprachigen Features.

Feature-Dateien haben die Endung .feature und befinden sich üblicherweise im Verzeichnis features. Wenn Cucumber ohne Parameter gestartet wird, sucht es automatisch nach einem Unterverzeichnis diesen Namens im aktuellen Verzeichnis, deshalb empfiehlt es sich, Cucumber im Verzeichnis eine Ebene über features zu starten.

Ein Feature enthält eine Beschreibung (Freitext, der nicht geparst wird) und ein oder mehrere Szenarios. Ein Szenario beschreibt einen Verhaltensaspekt des zu testenden Systems. Es trifft Aussagen darüber, wie sich das System verhält, wenn ein bestimmte Aktion ausgeführt wird – vorausgesetzt, dass bestimmte Vorbedingungen erfüllt sind.

Ein Szenario besteht aus einzelnen Schritten (Steps). Jeder Schritt ist ein Satz (oder Satzteil), der zu einer der drei bekannten BDD-Kategorien gehört:

  • Given (stellt eine Vorbedingung für das Szenario her),
  • When (führt eine Aktion im zu testenden System aus) and
  • Then (verifiziert das gewünschte Ergebnis)

Auf technischer Ebene ist es egal, ob man einem Schritt Given, When, Then, And oder But voranstellt (And und But können für alle drei Kategorien verwendet werden). Man könnte also Vorbedingungen mit Then herstellen und mit Given das gewünschte Ergebnis überprüfen, aber Szenarios, die die Konventionen korrekt verwenden, sind natürlich wesentlich lesbarer.

Benutzt man das oben erwähnte Language-Tag für eine deutsche Lokalisierung, lauten die Präfixe

  • Given: Angenommen/Gegeben sei/Gegeben seien
  • When: Wenn
  • Then: Dann
  • And: Und
  • But: Aber

Für jeden Step muss eine passende Step Definition existieren. Diese liegen im Verzeichnis features/step_definitions. Wir sehen uns diese später im Detail an.

Das erste Feature

Um das erste Cucumber-Feature zu erstellen, legen wir die Datei features/first.feature mit folgendem Inhalt an (statt den Code zu kopieren, kann auch der vorbereitete Branch benutzt werden: git checkout 01_first_feature).

Hinweis: Sämtlicher Code aus diesem Tutorial ist im Beispiel-Repository enthalten. Das Repository enthält mehrere Branches, die ungefähr den einzelnen Abschnitten des Tutorials entsprechen. Eine Liste der vorhandenen Branches lässt sich mit git branch -r anzeigen. Die Namen der Branches werden jeweils im weiteren Verlauf des Tutorials erwähnt, wenn sie relevant werden.

#encoding: utf-8
Feature: Showcase the simplest possible Cucumber scenario
  In order to verify that cucumber is installed and configured correctly
  As an aspiring BDD fanatic 
  I should be able to run this scenario and see that the steps pass (green like a cuke)
 
  Scenario: Cutting vegetables
    Given a cucumber that is 30 cm long
    When I cut it in halves
    Then I have two cucumbers
    And both are 15 cm long

Führt man dieses Feature nun aus, indem man im Hauptverzeichnis des Projekts (das Verzeichnis direkt oberhalb von features) das Kommando cucumber (oder besser: bundle exec cucumber) absetzt, sollte die Ausgabe in etwa so aussehen:

#encoding: utf-8
Feature: Showcase the simplest possible cucumber scenario
  In order to verify that cucumber is installed and configured correctly
  As an aspiring BDD fanatic 
  I should be able to run this scenario and see that the steps pass (green like a cuke)

  Scenario: Cutting vegetables          # features/first.feature:8
    Given a cucumber that is 30 cm long # features/first.feature:9
    When I cut it in halves             # features/first.feature:10
    Then I have two cucumbers           # features/first.feature:11
    And both are 15 cm long             # features/first.feature:12

1 scenario (1 undefined)
4 steps (4 undefined)
0m0.003s

You can implement step definitions for undefined steps with these snippets:

Given(/^a cucumber that is (\d+) cm long$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

When(/^I cut it in halves$/) do
  pending # express the regexp above with the code you wish you had
end

Then(/^I have two cucumbers$/) do
  pending # express the regexp above with the code you wish you had
end

Then(/^both are (\d+) cm long$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

If you want snippets in a different programming language,
just make sure a file with the appropriate file extension
exists where cucumber looks for step definitions.

Cucumber wiederholt zuerst das Feature, danach wird das Resultat ausgegeben. Cucumber beschwert sich hier zurecht darüber, dass wir die benutzten Schritte noch nicht definiert haben. Als besonderen Service gibt Cucumber noch Vorschläge aus, wie wir die Schritte implementieren können. Wie nett! Es vermutet außerdem noch (korrekterweise), dass die Zahlen in den Schritten variabel sind.

Wir können also Cucumbers Vorschläge versuchsweise in eine Step Definition Datei kopieren. (Tatsächlich führe ich Cucumber ab und an absichtlich mit undefinierten Schritten aus, um die Vorschläge als Vorlagen für die Step Definitions zu verwenden.)

Steps und Step Definitions

Legen wir also das Verzeichnis features/step_definitions an und in diesem Verzeichnis die Datei first_steps.rb. Step Definitions werden in Ruby implementiert – zumindest in der Ruby-Variante von Cucumber. Wie bereits erwähnt gibt es Cucumber-Portierungen für einer Vielzahl von Programmiersprachen, es besteht also durchaus die Möglichkeit, Step Definitions in der Sprache zu implementieren, mit der man am vertrautesten ist. In diesem Beitrag wird allerdings das „Original“ und damit Ruby verwendet. Auch wer bisher noch nichts mit Ruby zu tun hatte, hat keinen Grund zur Besorgnis: Step Definitions müssen nicht allzu kompliziert sein und wir werden die benötigten Ruby-Sprachmittel unterwegs erläutern.

Hier nun der Code für features/step_definitions/first_steps.rb. (Der Name der Datei ist eigentlich irrelevant, alle .rb Dateien in features/step_definitions werden herangezogen.)

#encoding: utf-8
Given /^a cucumber that is (\d+) cm long$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end
 
When /^I cut it in havles$/ do
  pending # express the regexp above with the code you wish you had
end
 
Then /^I have two Cucumbers$/ do
  pending # express the regexp above with the code you wish you had
end
 
Then /^both are (\d+) cm long$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Wenn wir nun Cucumber erneut ausführen, sieht die Ausgabe schon geringfügig anders aus (nicht unbedingt besser, aber anders):

#encoding: utf-8
Feature: Showcase the simplest possible cucumber scenario
  In order to verify that cucumber is installed and configured correctly
  As an aspiring BDD fanatic 
  I should be able to run this scenario and see that the steps pass (green like a cuke)

  Scenario: Cutting vegetables          # features/first.feature:8
    Given a cucumber that is 30 cm long # features/step_definitions/first_steps.rb:1
      TODO (Cucumber::Pending)
      ./features/step_definitions/first_steps.rb:2:in `/^a cucumber that is (\d+) cm long$/'
      features/first.feature:9:in `Given a cucumber that is 30 cm long'
    When I cut it in halves             # features/step_definitions/first_steps.rb:5
    Then I have two cucumbers           # features/step_definitions/first_steps.rb:9
    And both are 15 cm long             # features/step_definitions/first_steps.rb:13

1 scenario (1 pending)
4 steps (3 skipped, 1 pending)
0m0.005s

Die Zusammenfassung informiert uns, dass mindestens einer der Schritte den Status pending hat und daher das ganze Szenario als pending markiert wird. Ein Szenario wird komplett übersprungen, wenn einer seiner Schritte fehlschlägt oder als pending markiert ist, daher versucht Cucumber erst gar nicht, die anderen Schritte auszuführen, sondern zählt die verbleibenden drei Schritte als skipped.

Bevor wir nun die Step Definitions tatsächlich mit Leben füllen, sollten wir uns first_steps.rb etwas näher anschauen. Wenn man noch nie zuvor Cucumber Step Definitions gesehen hat, sieht der Code vielleicht etwas ungewöhnlich aus. Die einzelnen Step Definitions sehen ein wenig wie Methoden aus – tatsächlich sind sie nichts anderes als Ruby-Methoden und als Code innerhalb der einzelnen Step Definitions lässt sich normales Ruby verwenden. Die Methoden-Signaturen sehen allerdings seltsam aus – sie enthalten jeweils einen regulären Ausdruck. Das ist ein nützliches Feature: Wenn Cucumber nach der Step Definition für einen Step in einem Szenario sucht, werden alle Step Definition Dateien (alle .rb Dateien in features/step_definitions, inklusive Unterverzeichnisse) durchsucht. Wird ein passender regulärer Ausdruck gefunden, wird die dazugehörige Step Definition ausgeführt. Übrigens: Wenn Cucumber mehr als einen passenden regulären Ausdruck findet, beschwert es sich (ambiguous match), statt einfach die erstbeste Definition auszuführen.

Dieser Mechanismus bringt mehrere Vorteile mit sich. Einer dieser Vorteile ist eine gut lesbare und flexible Parametrisierung von Step Definitions. Wir haben das bereits gesehen:

Given /^a cucumber that is (\d+) cm long$/ do |arg1|

Der Teil der Zeichenkette, der der Capturing Group – (\d+) – zugeordnet wurde, wird der Step Definition als Parameter arg1 übergeben (wir sollten diese Parameter vermutlich in length umbenennen). \d+ passt zu einer oder mehreren Ziffern, somit kann hier also ein numerischer Wert übergeben werden.

Außerdem eröffnet die Verknüpfung von Steps mit Step Definitions über reguläre Ausdrücke die Möglichkeit, Szenarios zu schreiben, die sich sehr flüssig lesen lassen, ohne dafür Step Definitions duplizieren zu müssen. Dazu ein kleines Beispiel: Wir könnten den zweiten Step folgendermaßen umschreiben:

When /^I (?:cut|chop) (?:it|the cucumber) in (?:halves|half|two)$/ do
  pending # express the regexp above with the code you wish you had
end

Damit wäre jeder der folgenden Schritte möglich:

When I cut the cucumber in halves
When I chop the cucumber in half
When I cut it in two

(In Bezug auf das Zerkleinern von Gemüse ist „cutting in halves“ besseres Englisch als „cutting in half“, aber darum geht es hier nicht.)

Wir benutzen hier Non-Capturing Groups (diese fangen mit "(?:") an) um die verschiedene Formulierungen zu unterstützen.

Wenn man allerdings einmal damit anfängt, seine Step Definitions dergestalt zu erweitern, sollte man darauf achten, es nicht zu übertreiben, sondern nur die Variationen bereitzustellen, die man auch tatsächlich gerade benötigt – sonst läuft man Gefahr, sich mehr mit den regulären Ausdrücken auseinanderzusetzen, statt sein System zu testen. Außerdem hat man noch weitere Möglichkeiten, Duplizierungen bei den Step Definitions zu vermeiden. Man kann normale Ruby-Methoden in den Step File einfügen und diese von den Step Definitions aus aufrufen. Weiterhin kann man andere Ruby-Module per require einbinden und deren Code so wiederverwenden. Schließlich lassen sich sogar Step Definitions von anderen Step Definitions aus aufrufen.

Step Definitions implementieren

Momentan enthalten alle Steps Definitions nur einen Aufruf der Methode pending. Dies ist im Prinzip ein Todo-Marker. Mit pending lässt sich das Szenario zuerst ohne implementierte Step Definitions schreiben und dann ein Step nach dem anderen definieren, bis das Szenario erfolgreich durchläuft. Dieses Vorgehen passt auch gut zu einem ATDD-/Outside-In-Ansatz, bei dem man die Implementierung des Systems mit Akzeptanztests treibt:

  1. Zuerst das Cucumber-Feature schreiben (mit pending Steps),
  2. die Step Definition des ersten Steps implementieren,
  3. genau die Funktionalität im getesteten System implementieren, die benötigt wird, damit dieser Step durchläuft,
  4. diesen Ablauf jeweils für die restlichen Steps wiederholen.

Nach langer Vorrede nun die Implementierung der Step Definitions. Der Code ist auch per git checkout 02_step_definitions verfügbar.

Given /^a cucumber that is (\d+) cm long$/ do |length|
  @cucumber = {:color => 'green', :length => length.to_i}
end
 
When /^I (?:cut|chop) (?:it|the cucumber) in (?:halves|half|two)$/ do
  @choppedCucumbers = [
    {:color => @cucumber[:color], :length => @cucumber[:length] / 2},
    {:color => @cucumber[:color], :length => @cucumber[:length] / 2}
  ]
end
 
Then /^I have two cucumbers$/ do
  @choppedCucumbers.length.should == 2
end
 
Then /^both are (\d+) cm long$/ do |length|
  @choppedCucumbers.each do |cuke|
    cuke[:length].should == length.to_i
  end
end

Wenn Cucumber nun erneut ausgeführt wird, sollte folgendes Ergebnis ausgegeben werden:

#encoding: utf-8
Feature: Showcase the simplest possible cucumber scenario
  In order to verify that cucumber is installed and configured correctly
  As an aspiring BDD fanatic 
  I should be able to run this scenario and see that the steps pass (green like a cuke)

  Scenario: Cutting vegetables          # features/first.feature:8
    Given a cucumber that is 30 cm long # features/step_definitions/first_steps.rb:3
    When I cut it in halves             # features/step_definitions/first_steps.rb:7
    Then I have two cucumbers           # features/step_definitions/first_steps.rb:14
    And both are 15 cm long             # features/step_definitions/first_steps.rb:18

1 scenario (1 passed)
4 steps (4 passed)
0m0.005s

Grandios! Nun werden alle Schritte in grün ausgegeben – damit sieht die Ausgabe (mit viel Fantasie) wie eine Cucumber aus (wodurch auch erklärt wäre, wie das Framework zu seinem Namen kommt). Betrachten wir die Implementierung der Schritte im Einzelnen: Ein Bezeichner, der mit einem @ beginnt, ist in Ruby eine Instanz-Variable. Diese müssen nicht vorab deklariert werden; wenn sie zur Laufzeit zugewiesen werden, ist die Instanzvariable ab diesem Zeitpunkt definiert. Damit ist @cucumber in allen Schritten sichtbar und wir können Zustand über Step-Grenzen hinweg halten. Die Ausdrücke, die in geschweifte Klammern gefasst sind, sind Hashes (aka Dictionaries oder assoziateve Arrays, vergleichbar mit java.util.Map); die Bezeichner, die mit einem Doppelpunkt beginnen, sind hier die Schlüssel (der Doppelpunkt macht sie zu Symbolen, aber das ist momentan nebensächlich).

Die Ausdrücke @choppedCucumbers.length.should == 2 und cuke[:length].should == length.to_i bedürfen evtl. einer Erläuterung. Wir benutzen hier das RSpec Modul Spec::Expectations. In features/support/env.rb haben wir die Zeile require 'rspec/expectations', dadurch ist dieses Modul in jedem unserer Step Files (also auch first_steps.rb) verfügbar. (Alle Dateien in features/support/ werden automatisch vor dem Ausführen des ersten Szenarios geladen.) Mit diesem Modul haben wir die Möglichkeit, Erwartungen an beliebige Objekte zu formulieren, in dem es allen Objekten Methoden hinzufügt (Monkey Patching). In diesem Fall benutzen wir Object Expectations, damit stehen uns auf allen Objekten die Methoden should == sowie should_not == (und noch einige mehr) zur Verfügung. Eigentlich ist das aber nichts anderes als Syntactic Sugar, eine etwas elegantere Möglichkeit um unsere Erwartung auszudrücken, das ein bestimmter Wert gleich einem zweiten Wert ist. Dies ist möglich, da die besagten Methoden allen Objekten hinzugefügt werden und weil in Ruby tatsächlich alles ein Objekt ist, selbst numerische Werte wie die Länge eines Arrays – daher können wir die Method should == auf der Eigenschaft length des Arrays @choppedCucumbers aufrufen.

Die Implementierungen der Step Definitions sind recht albern – tatsächlich ist das ganze Szenario hinreichend albern, außerdem haben wir in diesem Beispiel noch nicht mal produktiven Code getestet. Sämtlicher Code befindet sich im Step File. Trotzdem sollten anhand des Beispiels einige grundlegende Konzepte von Cucumber klar geworden sein.

Anmerkung: Wir mussten an zwei Stellen die Methode to_i benutzen, da der Parameter, der durch die Capturing Group im regulären Ausdruck entgegengenommen wird, immer ein String ist. Wir könnten diese Unschönheit umgehen, indem wir Step Argument Transformer benutzen.

Gut, und jetzt?

Damit sind die Grundlagen von Cucumber abgedeckt – und wir können aufhören, mit albernen Beispielen zum Halbieren von Gurken herumzuspielen. Im zweiten Teil werden wir eine echte Web-Anwendung testen.

Bastian Krol entwickelt seit über 15 Jahren Enterprise-Systeme und Open-Source-Software. Seine Schwerpunkte sind Java, JavaScript, Node.js und Hypermedia-APIs.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.