Mit dem Test-Framework Cucumber – englisch-botanisch für Gurke – formulieren Entwickler und Produktabteilung gemeinsam Testfälle, und zwar nicht als Programmcode, sondern in leserlichem Englisch oder Deutsch. Dem anfänglich skeptischen Perlmeister schmeckt’s.
Ich bin Praktiker und Realist – eine ganz natürliche Kombination, wie ich finde. Beim Thema Entwickler-Frameworks für Nicht-Programmierer äußert sich das so: Wann immer ein neues Softwareprodukt behauptet hat, mit Mausklicks oder beschreibendem Text statt mit Code “Programmieren” zu ermöglichen, winkte ich ab. Erfahrungsgemäß lassen sich mit umgangssprachlichen Mitteln zwar oft einfache Lösungen zu Papier bringen, mit steigender Komplexität der Aufgabe stießen aber alle mir bekannten Projekte unweigerlich an Grenzen. Tritt dieser Fall ein, hat der Bausteinprogrammierer sein Projekt vergurkt und muss es von Grund auf mit einer richtigen Programmiersprache neu implementieren.
Perlmeister korrigiert sich
Als ich vor einigen Jahren zum ersten Mal vom Cucumber-Projekt [2] hörte, das Testfälle für Softwareprojekte in natürlicher Sprache beschreibt, dachte ich an eine Wiederauflage des Nicht-programmieren-müssen-Schmarrens und wandte mich reflexhaft ab und Dingen mit Substanz zu. Doch wie’s scheint, habe ich mich getäuscht, denn das Projekt hat zwischenzeitlich unter ernsthaft arbeitenden Entwicklern viel an Beliebtheit gewonnen.
Am Ende revidierte auch ich meine grundsätzliche Meinung. Den Ausschlag dafür gab die Lektüre des Buches zum Cucumber-Projekt [3]. Es stellt auf gut 300 Seiten ausführlich die Funktionen des Toolsets vor und zeigt, wie während eines aus dem Leben gegriffenen Softwareprojekts 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 so genannte Step-Definitionen, die mit regulären Ausdrücken im Text fischen und Aktionen auslösen, die in einer richtigen Programmiersprache kodiert sind. Die Step-Definition im Beispiel würde auf »Fetch the … stock quote« im Text anspringen, den Parameter »Facebook« extrahieren und sich anschicken mit einer Funktion aus einer Web-Library den Kurs der Facebook-Aktie vom Server einzuholen.
Meist sind Cucumbers Step-Definitionen in Ruby geschrieben, doch das Toolset unterstützt auch Java oder Dotnet. Mit dem CPAN-Modul Test::BDD::Cucumber existiert zum Herumexperimentieren sogar ein mehr oder weniger zufriedenstellend funktionierender Perl-Port.
Online PLUS
In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/2014/03/plus
Gute Etikette
Cucumber verpackt nicht nur eintönigen Testcode in umgangssprachlich formulierte Scenarios, sondern animiert generell zum so genannten Behavior Driven Development (BDD, verhaltensgetriebene Software-Entwicklung). Dabei schreibt der Programmierer nicht nur den Test einer Funktion vor der eigentlichen Implementierung (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 außerdem dazu, dass nicht nur der Programmcode den Anforderungen leicht wartbarer Software genügt, sondern auch der Testcode. Denn wer Tests nach der Cowboy-Coding-Methode abfasst, muss sich nicht wundern, dass Änderungen im Pflichtenheft mehr Arbeit zum Richten der Testsuite erfordern als im eigentlichen Projektcode.
Wer die Steps-Definitionen in mehreren Scenarios erneut verwendet, verhindert aktiv Code-Duplizierung und baut damit zukünftigen Wartungsproblemen vor. Auch trennt Cucumber die Testvorbereitung von der Ausführung, was während des Testlaufs dazu führt, dass die Suite vor jedem Testfall das Testgerüst jedes Mal einreißen und neu aufbauen kann, um sicherzugehen, 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 der Entwickler mit mehreren Testfällen (Scenarios) prüfen wird. Damit Cucumber sie als solche erkennt, muss die Datei in einem Verzeichnis namens »features« liegen und die Endung ».feature« tragen.
Schlüsselwörter mit abschließendem Doppelpunkt (»Feature:« , »Background:« , »Scenario:« ) leiten die einzelnen Abschnitte in der Feature-Datei ein, ihr Inhalt ist 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 – diese jedes Mal erneut einzutippen würde gegen die Regel “Don’t repeat yourself!” verstoßen. Der »Background:« -Abschnitt definiert daher Aktionen, die vor dem Start jedes Scenario 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.
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
Scenarios noch ohne Aktionen
Ist Cucumber installiert (siehe Abschnitt “Installation”), kann der ungeduldige Entwickler die Testsuite zur Probe abfeuern. Dazu ruft er das Programm »cucumber« im Verzeichnis ü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 schablonenartigen Ruby-Code vorschlägt.
Jeder Texteintrag nach »Given« , »When« oder »Then« beschreibt ein Scenario. Interessanterweise behandelt Cucumber diese Schlüsselwörter alle gleich und führt nur die entsprechend zugewiesene Step-Definition aus. Die unterschiedlichen Bezeichnungen dienen lediglich dazu, die Anforderungen für die Benutzer möglichst verständlich zu formulieren.
Perl ohne Glanz
Der Perl-Implementierung unter Zuhilfenahme des CPAN-Moduls Test::BDD::Cucumber [4] fehlt die Eleganz seines Vorbilds. Um die Testsuite zu starten, ruft der Perl-Programmierer nicht wie im Original das Programm »cucumber« auf, sondern das dem Modul beiliegende Skript »pherkin« , dessen Name wohl eine Synthese aus “Perl” und “Gherkin” (Gürkchen) ist. Die Ausgabe in Abbildung 2 macht mit grauem Text bei einem nicht funktionierenden Scenario klar, welche Step-Definitionen fehlen.
Listing 2 zeigt die Step-Definition, die den Text nach dem »Given« -Ausdruck in der Background-Beschreibung aufschnappt. Die Datei muss die Endung ».pl« aufweisen und in einem Verzeichnis namens »step_definitions« unterhalb von »features« liegen. Der in Zeile 6 zwischen »Given qr/« und »/« eingeschlossene reguläre Ausdruck sucht in allen Scenarios 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 auf dem System die Klasse mit »use Yahoo::FinanceQuote« laden lässt.
Listing 2
basic_steps.pl-Definition 1
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 };
Die Hilfsfunktion »use_ok()« stammt aus dem Fundus des Perl-Moduls Test::More und gibt wie im Perl’schen 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 passend grün oder rot.
Abbildung 3 zeigt, dass die vor dem eigentlichen Scenario ausgeführte Background-Anweisung nun grün eingefärbt erscheint, während die zwei folgenden Scenarios (»When […]« und »Then […]« ) noch grau ausschauen, also undefiniert sind.
Listing 3 liefert den Rest der Step-Definitionen nach, 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 dafür zu tun. Die beiden Schritte des Scenario »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 Scenario-Text vorgegebenen.
Listing 3
basic_steps.pl-Definition 2
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 };
Ein Austausch mit Kontext
Beide Steps 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 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 die ein anderer braucht.
In Listing 3 legt der »When« -Schritt die per »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 in Zeile 24 von dort wieder raus. 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.
Ausbaufähige Scenarios
Mit den Schrittdefinitionen aus Listing 3 rattern nun, wie in Abbildung 4 zu beobachten, alle bisher definierten Scenarios fehlerlos durch. Da Cucumber die Feature-Beschreibung von der Schrittdefinition trennt, eignet sich Letztere auch für weitere Scenarios. Um zum Test nicht nur den Kurs der Facebook-Aktie, sondern auch den weiterer Wertpapiere einzuholen und zu prüfen, kommt in Listing 4 ein weiteres Feature, ebenfalls im Verzeichnis »features« , zum Einsatz.
Das Listing zeigt, wie die Zeilen einer mit Pipe-Symbolen geschriebenen Ascii-Tabelle nach dem Schlüsselwort »Examples:« je einen neuen Testfall definieren. So soll der Kurs der Facebook-Aktie höher als 10 Dollar und der des Amazon-Papiers höher als 400 sein. Wie ein Blick in die Börsenseiten offenbart, verfehlt der Internethändler dieses Ziel knapp – mit rund 397 Dollar zu Redaktionsschluss. Der Testfall ist offensichtlich falsch definiert, ein großzügiger Spielraum – zum Beispiel “größer als 1” – wäre sinnvoll, damit das Skript auch in ein paar Jahren noch funktioniert.
In Abbildung 5 leuchtet das fehlgeschlagene Scenario denn auch rot. Wie sich am ergrauten nachfolgenden Scenario 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.
Beim Definieren von Tabellen unterscheidet sich das CPAN-Modul übrigens vom Original – vermutlich unbeabsichtigt: »cucumber« verweigert »Examples:« in einem »Scenario:« , sondern akzeptiert sie nur in einem Abschnitt, der mit »Scenario Outline:« beginnt.
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 |
Installation
Das vorgestellte Cucumber-CPAN-Modul installiert der Perl-Enthusiast mit »cpan Test::BDD::Cucumber« . In Test::BDD::Cucumber::Manual::Tutorial findet er zudem ein kurzes Tutorial. Das Modul läuft unabhängig von dem Originalcode des Cucumber-Projekts, der als Ruby-Gem erhältlich wäre. Um es ins System zu hieven, ruft der Anwender auf einem System mit installiertem Ruby-Interpreter samt »ruby-dev« -Paket die beiden Kommandos »gem install gherkin« und »gem install cucumber« auf.
Im Gegensatz zum Perl-Modul bietet der originale Cucumber-Code auch die Möglichkeit, die Features in anderen Sprachen zu beschreiben, auf Deutsch beispielsweise. Dann wird vielleicht aus dem »Given« ein »Falls« . Die Step-Definitionen durchforsten dann deutschen Text. »cucumber –language« unterstützt über 40 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 eröffnet es für Entwickler und fürs Produktmanagement die Chance, gemeinsam am Feature-Dokument zu arbeiten und gleichzeitig automatisch zu testen. Das Gurken-Framework fördert schon während der Entwicklung die Dialogbereitschaft der beiden Parteien und hilft damit, durch Missverständnisse verursachte kostspielige Produktfehler zu vermeiden.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/03/Perl
- Cucumber: http://cukes.info
- Matt Wynne, Aslak Hellesoy, “The Cucumber Book: Behaviour-Driven Development for Testers and Developers”: Pragmatic Programmers, Februar 2012)
- Test::BDD::Cucumber: http://search.cpan.org/~bdr/Test-BDD-Cucumber/












