Gurkenflieger (Linux-Magazin, März 2014)

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.

Getäuschter Meister

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.

Gute Etikette

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.

Freundlicher Helfer

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.

Listing 1: basic.feature

    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.

Perl weniger poliert

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.

Listing 2: basic_steps.pl

    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.

Austausch mit Kontext

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.

Listing 3: basic_steps.pl

    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.

Ausbaufähig

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.

Listing 4: table.feature

    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.

Installation

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.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/03/Perl

[2]

"The Cucumber Book: Behaviour-Driven Development for Testers and Developers", Pragmatic Programmers, Matt Wynne, Aslak Hellesoy, Februar 2012)

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.