Mit dem Test-Framework Cucumber können Entwickler und Produktabteilung gemeinsam Testfälle formulieren, und zwar nicht als Programmcode, sondern in leserlichem Englisch oder Deutsch.
Verspricht ein neues Softwareprodukt mal wieder, mit Mausklicks oder beschreibendem Text statt mit Programmcode zu programmieren, gehen sofort meine Alarmglocken los. Erfahrungsgemäß lassen sich mit umgangssprachlichen Mittel zwar oft einfache Lösungen zu Papier bringen, die aber irgendwann unweigerlich an ihre Grenzen stoßen. Tritt dieser Fall ein, steht der Bausteinprogrammierer wie der Ochs vorm Berg und muss wieder von vorn anfangen, und doch von Grund auf in einer richtigen Programmiersprache programmieren.
Als ich vor einigen Jahren zum ersten Mal vom Cucumber-Projekt (http://cukes.info) hörte, das Testfälle für Softwareprojekte in natürlicher Sprache beschreibt, dachte ich sein Schicksal sei besiegelt und wandte mich reflexartig ab. Doch scheinbar habe ich mich getäuscht, denn das Projekt erfreut sich steigender Beliebtheit. Das Buch zum Cucumber-Projekt ([2]) stellt auf über 300 Seiten ausführlich die Funktionen des Toolsets vor und zeigt, wie während eines aus dem Leben gegriffenen Software-Projekts Schritt für Schritt eine nützliche Testsuite entsteht.
Neben der umgangssprachlichen Beschreibung eines Testfalls (zum Beispiel "Fetch the Facebook stock quote from the server") nutzt Cucumber sogenannte Step-Definitionen, die mit regulären Ausdrücken im Text fischen und Aktionen auslösen, die in einer richtigen Programmiersprache codiert sind. Die Step-Definition im Beispiel würde auf "Fetch the ... stock quote" im Text anspringen, den Parameter "Facebook" extrahieren und sich anschicken, mittels einer Funktion aus einer Web-Library den Kurs der Facebook-Aktie vom Server einzuholen.
Meist sind die Step-Definitionen in Cucumber in Ruby geschrieben, doch das Toolset unterstützt auch Java oder .NET. Mit dem CPAN-Modul Test::BDD::Cucumber existiert sogar ein mehr oder weniger zufriedenstellend funktionierender Perl-Port zum Herumspielen.
Cucumber verpackt nicht nur eintönigen Testcode in umgangssprachlich formulierte Szenarien, sondern animiert generell zum sogenannten "Behavior-Driven-Development" (BDD). Dabei schreibt der Programmierer nicht nur den Test vor der eigentlichen Implementierung einer Funktion ("Test-Driven-Development", TDD), sondern legt zugleich fest, dass der Test ein gewünschtes Verhalten der Software nach außen sicherstellt, das auch der Produktmanager versteht.
Durch seine Struktur animiert Cucumber zudem dazu, dass nicht nur der Programmcode den Anforderungen leicht wartbarer Software genügt, sondern auch der Testcode. Wer Tests nach der Cowboy-Coding-Methode erstellt, steht kurz oder lang vor dem Problem, dass Änderungen im Pflichtenheft mehr Arbeit zum Richten der Testsuite erfordern als im eigentlichen Projektcode.
Wer die Steps-Definitionen in mehreren Szenarien wiederverwendet, verhindert aktiv Code-Duplizierung und baut damit zukünftigen Wartungsproblemen vor. Auch trennt Cucumber die Testvorbereitung und von der Ausführung, was während des Testlaufs dazu führt, dass die Suite vor jedem Testfall das Testgerüst jedesmal einreißen und neu aufbauen kann, um sicher zu gehen, dass kein "Leaky Scenario" vorliegt, also ein Testfall den nächsten unerwünscht beeinflusst.
Listing 1 zeigt eine vollständige Testbeschreibung eines Moduls zum
Einholen von Aktienkursen. Die Datei beschreibt ein "Feature", also eine
geforderte Funktion des Systems, die mit mehreren Testfällen ("Scenarios")
geprüft wird. Damit Cucumber sie als solche erkennt, muss die Datei
in einem Verzeichnis namens features
liegen und die Endung
.feature
tragen.
01 Feature: Simple tests of Yahoo::FinanceQuote 02 As a developer using Yahoo::FinanceQuote 03 I want to retrieve stock quotes 04 05 Background: 06 Given a usable Finance::YahooQuote class 07 08 Scenario: Retrieve Facebook 09 When retrieving the quote for ticker symbol "FB" 10 Then the numeric value is greater than 10
Die einzelnen Abschnitte in der Feature-Datei werden jeweils durch
Schlüsselworte mit abschließendem Doppelpunkt eingeleitet ("Feature:",
"Background:", "Scenario:") und ihr Inhalt mit zwei Leerzeichen
eingerückt. Die Funktionen eines Features prüft Cucumber mit
mehreren voneinander
unabhängigen Scenarios. In der Praxis erfordert jeder Testfall einige
Schritte zur Initialisierung, und diese jedes Mal erneut einzutippen
würde gegen die Regel "Don't repeat yourself" verstoßen. Der Abschnitt
Background
definiert deswegen Aktionen, die vor dem Start jedes
Szenarios ablaufen sollen. Im Beispiel prüft der Programmierer, ob
das zum Einholen der Aktienkurse verwendete CPAN-Modul Yahoo::FinanceQuote
überhaupt installiert ist und das Skript eine Instanz der Klasse
erzeugen kann.
Ist cucumber
installiert (siehe Abschnitt "Installation"), kann
der ungeduldige Entwickler die Testsuite nun schon einmal probeweise
abfeuern. Dazu ruft er das Programm cucumber
im Verzeichis über
dem features
-Verzeichnis auf, in dem die Datei basic.feature
aus
Listing 1 liegt.
Freilich hat cucumber
zu diesem Zeitpunkt noch keine Ahnung, welche Aktionen
es aufgrund der Feature-Beschreibung ausführen muss, da die Step-Definitionen
noch fehlen, aber es versucht, auszuhelfen, indem es wie in Abbildung 1
gezeigt, schablonenartigen Ruby-Code vorschlägt.
Abbildung 1: Das Tool cucumber hilft dem Programmierer bei fehlenden Step-Definitions auf die Sprünge. |
Jeder Texteintrag nach "Given", "When" oder "Then" beschreibt ein Szenario.
Interessanterweise behandelt cucumber
diese Schlüsselworte alle gleich und
führt nur die entsprechend zugewiesene Step-Definition aus.
Die unterschiedlichen Kennworte dienen lediglich dazu, die Anforderungen
möglichst verständlich zu formulieren.
Die Perl-Implementierung mittels des CPAN-Moduls Test::BDD::Cucumber gibt sich
da weniger kundenfreundlich.
Um die Testsuite zu starten, ruft der Perl-Programmierer nicht wie im
Cucumber-Original das Programm cucumber
auf, sondern das dem Modul
beiliegende Skript pherkin
(stammt wohl von "gherkin" (Gürkchen) mit
"P" für "Perl").
Abbildung 2 zeigt, dass die Ausgabe wegen der fehlenden Step-Definitionen
lediglich das nicht funktionierende Szenario in grauem Text ausgibt.
Abbildung 2: Ohne Step-Definitionen gibt pherkin nur die Szenario-Beschreibung in grau aus. |
Listing 2 zeigt die Step-Definition, die den Text nach dem Given
-Ausdruck
in der Background-Beschreibung aufschnappt. Sie muss in einem Verzeichnis
namens step_definitions
unterhalb von features
liegen und die Endung
.pl
aufweisen. Der nach Given
in Zeile 6 mit qr/.../
eingeleitete
reguläre Ausdruck sucht in allen Szenarios des Features nach einem
String "a usable ... class"
. Wird er fündig, liegt der mittels Klammern
in (\S+)
gruppierte Klassenname in der Variablen $1
, und
use_ok()
prüft in Zeile 7, ob sich die Klasse mit
use Yahoo::FinanceQuote
laden lässt.
Die Hilfsfunktion use_ok()
stammt
aus dem Fundus des Perl-Moduls Test::More und gibt wie im Perlschen
TAP-Format üblich "ok 1" aus falls alles gut geht und "not ok 1", falls
ein Fehler auftritt. Das Tool pherkin
versteht dieses Format und koloriert
den zugehörigen Feature-Text entsprechend grün oder rot.
Abbildung 3 zeigt, dass die vor dem eigentlichen Szenario ausgeführte
Background-Anweisung nun grün eingefärbt erscheint, während die zwei
folgenden Szenarios ("When ..." und "Then ...") noch grau, also
undefiniert sind.
1 #!/usr/local/bin/perl 2 use strict; 3 use warnings; 4 use Test::More; 5 6 Given qr/a usable (\S+) class/, sub { 7 use_ok( $1 ); 8 };
Abbildung 3: Nur die Background-Anweisung läuft fehlerfrei, den Szenarios fehlt noch die Step-Definition. |
Listing 3 zeigt den Rest der Step-Definitions, um
die Feature-Datei in Listing 1 vollständig abzuarbeiten.
Die Kommandos Given
, Then
, And
, und so weiter definiert
pherkin
vor dem Aufruf der Step-Definition, der Anwender braucht
nichts dazu zu tun. Die beiden Schritte des Szenarios "Retrieve Facebook"
hängen allerdings voneinander ab, denn der erste Schritt holt den
Kurs vom Server ab und der zweite vergleicht den numerischen Wert mit
einem im Szenario-Text vorgegebenen.
01 #!/usr/local/bin/perl 02 use strict; 03 use warnings; 04 use Test::More; 05 use Method::Signatures; 06 use Finance::YahooQuote; 07 08 Given qr/a usable (\S+) class/, func ($c) { 09 use_ok( $1 ); 10 }; 11 12 When qr/retrieving the quote for ticker symbol "(.*)"/, func($c) { 13 my( $symbol, $name, $quote ) = getonequote( $1 ); 14 $c->stash->{scenario}->{quote} = $quote; 15 ok $quote, "got quote"; 16 }; 17 18 Then qr/the numeric value is greater than (.+)/, func ($c) { 19 my $val = $1; 20 if( $c->stash->{scenario}->{quote} !~ /^[\d.]+$/ ) { 21 my $quote = $c->stash->{scenario}->{quote}; 22 fail "invalid quote value: $quote"; 23 } 24 ok $c->stash->{scenario}->{quote} > $val, "val > $val"; 25 };
Beide Schritte kommunizieren über
einen Kontext miteinander, den pherkin
in der Variablen $c
bereitstellt, wenn wie in Zeile 5 der Steps-Definition in Listing 3 das Modul
Method::Signatures
geladen ist und die aufzurufende Funktion statt mit
sub {}
wie in Listing 1 mit func($c) {}
deklariert ist. Die
Methode stash()
holt eine Referenz auf einen Speicherbereich hervor,
der unter dem Schlüssel scenario
Platz für Daten bietet, die in einem
Schritt anfallen und in einem anderen wieder gebraucht werden. In Listing 3
legt der When
-Schritt die mittels getonequote()
aus dem Fundus
des Moduls Finance::YahooQuote vom Yahoo-Server abgeholten Kurs unter
dem Schlüssel "quote"
im Kontext ab. Der Then
-Schritt pult ihn
von dort dann in Zeile 24 wieder hervor.
Die Funktion ok()
aus Test::More prüft in Zeile 15, ob ein Kurswert
ankam, und in Zeile 24 ob er größer als der im Text eingestellte Wert
ist.
Abbildung 4: Nach Abschluss der Steps-Definitions laufen alle Schritte des Szenarios nun erfolgreich durch. |
Mit den Schrittdefinitionen aus Listing 3 laufen nun, wie in Abbildung 4
gezeigt, alle bisher definierten Szenarien fehlerlos durch. Da Cucumber
die Feature-Beschreibung von der Schrittdefinition trennt, kann letztere
auch für weitere Szenarien zum Einsatz kommen. Um jetzt zum Beispiel nicht
nur den Kurs der Facebook-Aktie sondern auch den weiterer Wertpapiere
zu Testzwecken einzuholen und zu prüfen, kommt in Listing 4 ein weiteres
Feature, ebenfalls im Verzeichnis features
, zum Einsatz.
01 Feature: Multiple Yahoo::FinanceQuote quote fetch tests 02 03 Scenario: Retrieve several quotes 04 When retrieving the quote for ticker symbol "<ticker>" 05 Then the numeric value is greater than <min_value> 06 Examples: 07 | ticker | min_value | 08 | FB | 10 | 09 | AMZN | 400 | 10 | GOOG | 500 |
Das Listing zeigt, wie die Zeilen einer mit Pipe-Symbolen geschriebenen ASCII-Tabelle nach dem Schlüsselwort "Examples:" jeweils einen neuen Testfall definieren. So soll der Kurs der Facebook-Aktie höher als 10 und der der Amazon-Aktie höher als 400 sein. Wie ein Blick in die Börsenseiten offenbart, ist letzteres jedoch (noch) nicht der Fall, denn die Amazon-Aktie stand am Redaktionsschluss noch bei 397 Dollars. Der Testfall ist also offensichtlich falsch definiert, ein großzügiger Spielraum (wie zum Beispiel "größer als 1") wäre sinnvoll, damit das Skript auch in ein paar Jahren noch funktioniert.
Abbildung 5: Ein Szenario schlägt fehl und stoppt die Ausführung des Feature-Tests. |
In Abbildung 5 leuchtet das fehlgeschlagene Szenario
denn auch rot auf, und wie sich am ergrauten nachfolgenden Szenario ablesen
lässt, bricht cucumber
den Test eines Features nach einem einzigen fehlgeschlagenen
Teil ab. Korrigiert der Tester den Wert "300" auf "400", laufen alle
Tests problemlos durch, wie die grüne Ausgabe in Abbildung 6 offenbart.
Bei der Tabellendefinition unterscheidet sich das CPAN-Modul übrigens
wohl unabsichtlich vom Original, cucumber
akzeptiert "Examples:" nicht
in einem "Scenario:" sondern nur in einem Abschnitt, der mit
"Scenario Outline:" beginnt.
Abbildung 6: Alle Tests laufen nun fehlerfrei. |
Das verwendete Cucumber-CPAN-Modul kann der Perl-Enthusiast mittels "cpan
Test::BDD::Cucumber" installieren. In Test::BDD::Cucumber::Manual::Tutorial
findet sich ein kurzes Tutorial. Das Modul läuft unabhängig von dem
Originalcode des Cucumber-Projekts, der als Ruby-Gem erhältlich ist. Hierzu
ruft der Anwender auf einem System mit installiertem Ruby-Interpreter plus dem
ruby-dev
-Paket die Kommandos gem install gherkin
und gem install
cucumber
auf.
Im Gegensatz zum Perl-Modul bietet der Original-Cucumber-Code auch die
Möglichkeit, die Feature-Beschreibungen in anderen Sprachen wie zum
Beispiel Deutsch anzugeben. Statt "Given" steht dann dort zum
Beispiel "Falls" und die Step-Definitionen durchforsten deutschen Text.
Die Option --language
des Tools cucumber
unterstützt über 40
verschiedene Sprachen.
Wie in [2] ausgeführt liegt der Vorteil der Cucumber-Methode aber nicht primär im Schreiben der Testfälle in einer natürlichen Sprache. Vielmehr hat dies den Nebeneffekt, dass Entwicklungs- und Produktmanagementteams gemeinsam am Feature-Dokument arbeiten können, das gleichzeitig automatische Tests zulässt. Dieses Verfahren hilft, schon während der Entwicklung einen Dialog zwischen den beiden Parteien in Gang zu bringen, der hilft, durch Missverständnisse verursachte kostspielige Produktfehler von vornherein zu vermeiden.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/03/Perl
"The Cucumber Book: Behaviour-Driven Development for Testers and Developers", Pragmatic Programmers, Matt Wynne, Aslak Hellesoy, Februar 2012)