Am Anfang stand das Wort

von Dr. Mirko Seifert, Dr. Christian Wende und Dr. Tobias Nestler

Acceptance Test-Driven Development (ATDD) verbindet die Lesbarkeit natürlichsprachlicher Spezifikationen mit der Ausführbarkeit formaler Tests

Automatisiertes Testen bildet einen festen Bestandteil jedes modernen Entwicklungsprozesses. Entwickler schreiben Tests mit dem Ziel, die Funktionalität einer Anwendung sicherzustellen. Diese Tests werden meist in einer Programmiersprache (z. B. Java) erstellt. Daraus ergibt sich zwangsläufig eine Lücke zwischen den eigentlichen Anforderungen (wie sie ein Kunde formulieren würde) und dem Testcode, der die Anforderungen prüft (wie ihn der Entwickler schreibt). Acceptance Test-Driven Development (ATDD) schließt diese Lücke, indem Anforderungen weitestgehend in natürlicher Sprache formuliert werden, aber gleichzeitig ihre Ausführbarkeit sichergestellt wird. Ziel von ATDD ist es, verständliche, wartbare und zugleich ausführbare Testspezifikationen zu schreiben. Mirko Seifert, Christian Wende und Tobias Nestler (alle DevBoost GmbH) führen in die grundlegenden Prinzipien von ATDD ein und stellen Werkzeuge vor, die diesen Ansatz unterstützen.

acceptance test driven development

Mithilfe geeigneter Testframeworks, wie z. B. JUnit [1] oder TestNG [2], ist es relativ einfach, Anwendungen programmatisch zu testen. Man erzeugt zuerst eine passende Umgebung für den Test (z. B. eine initialisierte Testdatenbank), dann wird eine bestimmte Funktionalität der Anwendung aufgerufen (z. B. die Buchung eines Flugs) und danach werden die erwarteten Nachbedingungen geprüft (z. B. ob ein gültiges Ticket ausgestellt wurde). Dieses prinzipielle Vorgehen ist bekannt, aber trotzdem ist das Schreiben von Tests oft keine triviale Angelegenheit. Selbst bei Verwendung geeigneter Bibliotheken (z. B. zur Kommunikation mit einer Webanwendung) wird der Testcode schnell umfangreich und damit auch schwer wartbar. Darunter leidet nicht nur die Motivation, neue Tests zu schreiben, sondern auch die Qualität der Anwendung. Aus „relativ einfach“ wird schnell „ziemlich aufwendig“.

Obwohl der Testautomatisierung also technisch nichts im Wege steht, stellt sich die Frage, warum der Aufwand, Tests zu schreiben, immer noch so hoch ist. Um die Ursache dieses Problems zu erkennen, muss man einen Schritt zurücktreten und noch einmal darüber nachdenken, warum wir Anwendungen testen und woher die eigentlichen Inhalte für Tests stammen.

Wir schreiben Tests, um sicherzustellen, dass die Anforderungen an unsere Anwendung tatsächlich von der Implementierung, das heißt dem Code der Anwendung, umgesetzt werden. Die Automatisierung ist an dieser Stelle nur ein Vehikel, um der Situation Herr zu werden, dass sich unser Code ständig ändert und dadurch oft unvorhergesehene Effekte auftreten, die existierende Funktionalität beeinflussen. In jedem Fall stellen Tests in ihrem Kern eine technische Umsetzung der Anforderungen dar, die von Kunden kommuniziert wurden. Die essenzielle Information eines Testfalls ist daher die fachliche Anforderung, die er prüft, nicht der technische Ablauf, um sie zu prüfen. Üblicherweise steht aber eben genau der technische Ablauf und nicht die fachliche Anforderung im Vordergrund. Der Testcode spezifiziert diesen Ablauf, die eigentlichen Anforderungen sind nur indirekt, wenn überhaupt erkennbar.

Das Ziel von Acceptance Test-Driven Development (ATDD), ist es die Diskrepanz zwischen fachlichen Anforderungen (wie sie ein Kunde formulieren würde) auf der einen Seite und dem technischen Ablauf der Überprüfung der Anforderungen (wie sie ein Entwickler in einem Test manifestiert) auf der anderen Seite zu überwinden.

ATDD – Another kind of Test-Driven Development?

Acceptance Test-Driven Development ist eine spezielle, noch relativ junge Art des Test-Driven Development. Die ersten Veröffentlichungen zum Thema ATDD sind nicht viel älter als fünf Jahre. Statt „Acceptance Test-Driven Development“ werden oft auch die Bezeichnungen „Specification by Example“ oder „Behavior-Driven Development“ verwendet. Obwohl verschiedene Autoren durchaus Unterschiede zwischen den konkreten Vorgehensmodellen hinter diesen Bezeichnungen ausmachen, so handelt es sich im Kern um ähnliche Ansätze mit vergleichbaren Zielen.

Die oberste Zielstellung der Ansätze ist es, alle an der Erstellung einer Anwendung Beteiligten (Kunden, Fachexperten, Programmierer und Tester) näher zusammenzubringen. Der Austausch über die Anforderungen soll intensiviert werden. Dies darf aber nicht allein auf einer menschlichen Ebene passieren, sondern muss auch konkret technisch umgesetzt werden. Wenngleich die Kommunikation zwischen den verschiedenen Parteien Grundlage jeder vernünftigen Anforderungserfassung ist, so geht es bei ATDD darum, Anforderungen so zu erfassen, dass diese später direkt für die Qualitätssicherung, das heißt den Test der Software, genutzt werden können. Auf dem zweiten Teil soll in diesem Artikel der Fokus liegen.

Von der Anforderung zum Test ist es ein weiter Weg

Gojko Adzic beschreibt in seinem Buch „Bridging the Communication Gap“ [3] ausführlich, wie unterschiedlich ein und dieselbe Anforderung von verschiedenen Personen verstanden werden kann. Während Fachexperten sehr präzises Wissen über die Domäne der Anwendung haben, so fehlt ihnen der Blick für Grenzfälle, da diese in der Realität selten auftauchen. Programmierer erkennen diese Fälle oft schnell, da sie später zwangsläufig festlegen müssen, wie sich die Anwendung dabei verhalten soll. Tester achten zusätzlich darauf, dass alle nötigen Schritte, z. B. zur Abarbeitung eines Szenarios, besprochen und festgelegt werden, da sie genau diese Schritte später formalisieren müssen. Das Zusammenspiel der verschiedenen Beteiligten kann also geschickt genutzt werden, um die Vollständigkeit der Anforderungen zu verbessern.

Hat man alle Beteiligten an einem Tisch sitzen, stellt sich natürlich die Frage, wie man die erarbeiteten Anforderungen festhält. Die Beschreibung aller nötigen Merkmale einer Anwendung in Klartext, das heißt in natürlicher Sprache, scheint an dieser Stelle das einfachste Mittel zu sein. Um zu vermeiden, dass die Vorstellungen dann später bei der Umsetzung wieder auseinanderlaufen, sollte der Text möglichst keine Mehrdeutigkeiten besitzen. Das lässt sich am leichtesten vermeiden, wenn man Szenarien (auch Use Cases oder User Stories genannt) spezifiziert, zusammen mit konkreten Eingaben und den dazugehörigen erwarteten Ausgaben. Man beschreibt die Funktionalität der Anwendung anhand von anschaulichen Beispielen. So erklärt sich auch der Name „Specification by Example“.

Möchte man beispielsweise die Funktion eines Buchungssystems für Flüge beschreiben, so wäre die konkrete Eingabe der Name eines Passagiers sowie eine Flugnummer. Die erwartete Ausgabe wäre beispielsweise das Flugticket. Die Ausgabe würde in diesem konkreten Fall natürlich davon abhängen, wie viele andere Passagiere bereits auf dem Flug gebucht sind und welche Sitzplatzkapazität die Maschine hat.

Vereinfacht ausgedrückt könnte die Spezifikation eines solchen Szenarios demnach lauten: „Versucht der Passagier Max Mustermann, einen Platz auf dem Flug LH-1234 zu buchen, so erhält er ein gültiges Ticket.“ Je nach Testumgebung muss natürlich noch festgelegt werden, welcher Flugzeugtyp auf dem Flug zum Einsatz kommt und wie viele Passagiere bereits eingebucht wurden. Dies kann auf ähnliche Art und Weise ausgedrückt werden: „Der Flug LH-1234 wird mit einer Maschine vom Typ Boeing 737 durchgeführt. Es sind noch keine Passagiere auf dem Flug gebucht.“

Die Erstellung solcher Spezifikationen ist einfach, da man das Szenario direkt in natürlicher Sprache beschreiben kann. Die Spezifikation ist für alle Beteiligten verständlich und lesbar. Nicht umsonst werden die meisten Anforderungsdokumente auf genau diesem Weg (z. B. mit Microsoft Word) erstellt. An diesem Punkt endet die Anforderungsspezifikation aber leider oft. Fachexperten, Programmierer und Tester gehen wieder getrennte Wege, die Anforderungen werden nicht direkt weiterverwendet, sondern dienen primär als Gedankenstütze für alle Beteiligten. Um die erarbeiteten Spezifikationen direkt weiter zu nutzen, müsste man sie ausführen, das heißt direkt als Testfälle nutzen können. Genau hier setzen ATDD bzw. die entsprechenden Werkzeuge

Ausführen ja, aber wie?

Um unsere Szenariobeschreibung (z. B. „Versucht der Passagier Max Mustermann, einen Platz auf dem Flug LH-1234 zu buchen, so erhält er ein gültiges Ticket“) auszuführen, müssen wir dem Satz eine Bedeutung zuordnen, das heißt festlegen welcher (Test-)Code dem Satz entspricht. Ein Ansatz, dies umzusetzen, wäre es, jede Szenariobeschreibung direkt mit ein paar Zeilen Testcode zu verknüpfen (zu annotieren). Diese Vorgehensweise ist schon ein großer Schritt nach vorn, weil Testcode und Testspezifikation direkt miteinander verbunden sind. Allerdings müsste man dann den Testcode für jedes Szenario selbst schreiben. Die Übersetzung müsste manuell erfolgen, was mit hohem Aufwand und ggf. auch mit Fehlern verbunden wäre. Eine Wiederverwendung des Testcodes über Szenarien hinweg wäre nicht möglich.

Um diesen Problemen zu begegnen, gehen manche ATDD-Tools einen pragmatischen, aber sehr wirkungsvollen Weg. Statt den Testcode für jedes Szenario erneut zu schreiben, wird die Übersetzung mithilfe von Übersetzungsmustern beschrieben und dann automatisch ausgeführt. Ein Übersetzungsmuster besteht dabei immer aus einem Muster für die Testspezifikation und einem Codefragment für den zugehörigen Testcode (sogenannter Support-Code). Das Muster für die Testspezifikation muss auf die Szenariobeschreibung passen, damit der zugehörige Support-Code ausgeführt wird.

Beispielsweise kann folgendes Muster für die Testspezifikation definiert werden: „Versucht der Passagier #NAME einen Platz auf dem Flug #FLUGNUMMER zu buchen, so erhält er ein gültiges Ticket“. Dabei können die Platzhalter #NAME und #FLUGNUMMER in konkreten Spezifikationen durch beliebige Werte ersetzt werden. Mit den konkreten Werten kann dann der Support-Code parametrisiert und so wiederverwendet werden.

Wir werden später noch Beispiele für weitere Muster sehen.

Da die Muster variable Platzhalter enthalten, ist es möglich mit einem Muster mehrere Szenariobeschreibungen zu übersetzen. Außerdem können komplexe Testszenarien aus mehreren Sätzen aufgebaut sein, die dann mit unterschiedlichen Übersetzungsmustern in Testcode überführt werden.

Der Übersetzungsschritt kann entweder interpretativ geschehen oder mit Hilfe eines Codegenerators umgesetzt werden. Im ersten Fall liest das ATDD Werkzeug die Spezifikation ein, gleicht die Muster ab und führt dann direkt den Support-Code der passenden Muster nacheinander aus (z. B. durch Reflection-Aufrufe). Kommt ein Codegenerator zum Einsatz, so wird der Support-Code der passenden Muster zu einem Testfall zusammengesetzt. Der Testfall kann im einfachsten Fall ein normaler JUnit Test Case sein.

Die Idee ist gut, doch sind die Tools denn schon bereit?

Um die Arbeit im Kontext von ATDD möglichst komfortabel zu gestalten, braucht man natürlich die passenden Werkzeuge. Gerade im Eclipse-Umfeld sind wir es ja gewohnt, auf gute Unterstützung in der IDE zurückzugreifen. Natürlich gibt es auch genau hier bereits mehrere Tools, auf die man sich stützen kann. Es sollen hier exemplarisch drei Werkzeuge (Cucumber [4], Jnario [5] und NatSpec [6]) vorgestellt werden. Die drei Werkzeuge unterscheiden sich sowohl technologisch als auch konzeptionell, so dass man gut erkennen kann, wie unterschiedlich die Tool-Unterstützung für dasselbe Vorgehen aussehen kann.

Um die Werkzeuge hinsichtlich ihrer Arbeitsweise besser vergleichen zu können, bleiben wir bei unserem Beispiel mit der Flugbuchung. Wir nutzen dazu ein kleines Domänenmodell mit einigen wenigen Klassen (Flight, AirplaneType, Passenger, BookedSeat). Weiter nehmen wir an, dass diese Domänenklassen mithilfe der Java Persistence API (JPA) in einer relationalen Datenbank abgelegt werden sollen. Dies spiegelt eine typische Situation in Java-Enterprise-Projekten wider. Selbstverständlich stellen diese Annahmen keine Grundvoraussetzung für den generellen Einsatz von ATDD-Werkzeugen dar.

Cucumber

Das erste Werkzeug, das wir hier betrachten wollen, heißt Cucumber, wurde im Jahr 2008 von Aslak Hellesøy initiiert und hat seine Wurzeln in der Ruby-Welt [4]. Mittlerweile wurde Cucumber auch nach Java portiert, so dass es auch ohne Ruby-Kenntnisse benutzt werden kann. Cucumber ist im Kern ein Kommandozeilenwerkzeug, das Testfälle aus normalen Textfiles einliest und diese dann ausführt. Das Prinzip von Cucumber ist dabei so einfach wie genial. Testfälle, sogenannte Features, werden als Textdateien im Projekt abgelegt. Jede Feature-Datei kann dann mehrere Szenarien (Scenarios) und Anweisungen oder Schritte (Steps) enthalten. Diese Schritte werden in natürlicher Sprache definiert. Ein Beispiel für unser Flugbuchungsbeispiel ist in Listing 1 zu sehen. Wie man leicht erkennen kann, muss die Beschreibung eines Szenarios einem vorgegebenen Muster folgen, ist aber dennoch gut lesbar.

Um ein solches Szenario ausführen zu können, muss selbstverständlich definiert werden, welche Bedeutung die einzelnen Schritte haben. Hier kommen die sogenannten „Step Definitions“ ins Spiel. Diese ordnen dem natürlichsprachlichen Text den passenden Support-Code zu. Dieser Code wird als Support-Code bezeichnet, weil er die Ausführung der Szenarien unterstützt bzw. erst ermöglicht. Letztendlich verarbeitet Cucumber also sukzessive die Beschreibungen der Szenarien und versucht den passenden Support-Code zu finden. Ist dieser vorhanden, so wird er ausgeführt. Kann keine passende Support-Methode gefunden werden, so gibt Cucumber eine entsprechende Warnung aus.

Der Aufbau eines Szenarios folgt immer einer Grundstruktur, der sogenannten „Gherkin“Syntax. Dabei spielen verschiedene Schlüsselworte eine besondere Rolle. Einige davon waren bereits in Listing 1 zu sehen. So beginnt jedes Feature-File mit dem Schlüsselwort „Feature“ und jedes Szenario mit „Scenario“. Die einzelnen Schritte können dann jeweils mit „Given“, „When“, „Then“, „And“ oder „But“ eingeleitet werden. Es existieren noch weitere Schlüsselworte, z. B. „Background“, auf die hier aber nicht im Detail eingegangen werden soll.

Um den einzelnen Schritten konkreten Code zuordnen zu können, werden Step Definitions benötigt. Step Definitions sind spezielle Methoden, die mit Annotationen (@Given, @When, @Then usw.) versehen werden können. Als Argument für die Annotationen dienen reguläre Ausdrücke, auf die der Text in den Szenariobeschreibungen matchen muss. Für unser Beispiel sind diese Methoden in der Klasse BookFlightStepDef zusammengefasst und in Listing 2 zu sehen. Hier kann man leicht erkennen, wie der Zusammenhang zwischen den Testschritten (siehe Listing 1) und den aufzurufenden Methoden hergestellt wird.

Ausführen kann man Cucumber-Tests mit einem speziellen TestRunner für JUnit. Dazu legt man eine leere Testklasse an und versieht diese mit der Annotation @RunWith(Cucumber. class). Natürlich lassen sich Cucumber-Tests auch im Build-Prozess mit Ant oder Maven anstoßen. An dieser Stelle ist es wichtig zu erwähnen, dass Cucumber interpretativ arbeitet, das heißt keinen Code für die FeatureDateien generiert. Man muss beim Ausführen der Tests auf einem CI-System also lediglich darauf achten, dass alle Features und alle Step Definitions im Klassenpfad liegen.

Die Ausgestaltung der Step Definitions und der Features ist einem weitgehend freigestellt. Man kann sowohl sehr spezielle, große StepDefinition-Methoden schreiben oder die Szenarien aus vielen kleinen parametrisierbaren Schritten zusammensetzen. Hier gilt es das richtige Gefühl für die nötige Granularität zu finden. Letztendlich ist dies auch eine Frage,
an welcher Stelle man den Inhalt der Tests maßgeblich gestalten will, das heißt, ob man lange Feature-Dateien kurzen Step Definitions vorzieht oder umgekehrt.

Über die genannte Basisfunktionalität hinaus bietet Cucumber viele weitere Möglichkeiten. So können beispielsweise Szenarien in verschiedenen Sprachen definiert werden, Schritte über Szenarien hinweg wiederverwendet werden und Features lassen sich mithilfe von Tags strukturieren.

Die Java-Implementierung von Cucumber macht bzgl. der Testausführung und der Integration in existierende Build-Systeme einen recht ausgereiften Eindruck. Als EclipseNutzer wünscht man sich natürlich vor allem einen ausgereiften Editor für Feature-Files. Hier existieren bereits verschiedene Projekte auf GitHub, bei denen an entsprechenden Plugins gearbeitet wird [7, 8, 9].

Listing 1

Feature: Book flight
Scenario: Book a seat for a passenger
Given a passenger Max Mustermann
And a flight LH-1234 executed on a
Boeing 737
When a seat is booked
Then a valid ticket is issued

Listing 2
1. public class BookFlightStepdefs {
2.
3.   private AirlineDAO dao = new AirlineDAO(getClass());
4.   private Passenger passenger;
5.   private Flight flight;
6.   private boolean success;
7.
8.   @Given(„^a passenger (.+)$“)
9.   public void createPassenger(String name) throws Throwable {
10.     String[] parts = name.split(“ „);
11.     String firstname = parts[0];
12.     String lastname = parts[1];
13.     passenger = dao.createPassenger(firstname, lastname);
14.   }
15.
16.   @Given(„^a flight (.+) executed on a (.+)$“)
17.   public void createFlight(
18.     final String flightNumber, final String airplaneName) throws Throwable {
19.     dao.executeInTransaction(new ICommand() {
20.       @Override
21.       public void execute(IDBOperations operations) {
22.         flight = operations.createFlight(flightNumber);
23.         int seats = 0;
24.         if („Boeing 737“.equals(airplaneName)) seats = 137;
25.         AirplaneType airplane =           operations.createAirplaneType(airplaneName, seats);
26.         flight.setAirplane(airplane);
27.       }
28.     });
29.   }
30.
31.   @When(„^a seat is booked$“)
32.   public void bookSeat() throws Throwable {
33.     success = dao.bookSeat(passenger, flight);
34.   }
35.
36.   @Then(„^a valid ticket is issued$“)
37.   public void isTicketValue() throws Throwable {
38.     assertTrue(success);
39.   }
40. }

Jnario

Das zweite interessante Werkzeug im Kontext von ATDD heißt „Jnario“ und wurde von Sebastian Benz und Birgit Engelmann bei der BMW Car IT GmbH entwickelt. Das Vorgehen bei der Nutzung von Jnario erinnert auf den ersten Blick stark an Cucumber, beim genaueren Hinsehen zeigt sich aber, dass es doch einige wesentliche Unterschiede zwischen den beiden Tools gibt.

So erlaubt Jnario z. B. die Spezifikation von Features in der gewohnten Cucumber-Syntax, das heißt mit den Schlüsselworten „Feature“, „Scenario“ usw., es bietet aber weitere Möglichkeiten an. Beispielsweise können sogenannte „Specs“ benutzt werden, um ebenfalls das erwartete Verhalten von Anwendungen zu beschreiben. Im Gegensatz zu Features sind Jnario Specs meist direkt mit der Klasse assoziiert, deren Verhalten getestet werden soll, und nutzen zudem eine andere Syntax, das heißt auch andere Schlüsselworte.

Auch bei der Einbindung des Support-Codes geht Jnario einen anderen Weg als Cucumber. Statt Methoden mit speziellen Annotationen zu versehen, kann der Testcode direkt in die Feature- oder auch Spec-Dateien eingefügt werden. Hier bedient sich Jnario der XtendSyntax [10]. So können sowohl normaler JavaCode als auch die erweiterten Sprachkonzepte von Xtend genutzt werden, um den SupportCode zu schreiben. Ein Beispiel für das Flugbuchungsszenario mit Jnario ist in Listing 3 zu sehen. Hier wird allerdings der Cucumber-Style und nicht die Jnario-Spec-Syntax verwendet.

Man kann im Listing gut erkennen, wie der Support-Code direkt in die Beschreibung des Szenarios eingebettet wird. Der Support-Code kann im Jnario-Eclipse-Editor auch ausgeblendet werden.

Aus jeder Feature-Beschreibung generiert Jnario automatisch einen JUnit-Test, der den entsprechenden Code enthält und so einfach in Eclipse ausgeführt werden kann. Jnario arbeitet an dieser Stelle im Gegensatz zu Cucumber mit einem Codegenerator statt mit einem Interpreter. Dieser Ansatz bietet den Vorteil, dass man den resultierenden Testcode direkt inspizieren kann. Möchte man den generierten Code nicht unter Versionskontrolle stellen, aber die Tests auf einem CI-System ausführen, so muss dort der Jnario-Codegenerator natürlich ebenfalls ausgeführt werden. Dazu bietet Jnario ein entsprechendes Maven-Plugin an.

Neben den genannten Möglichkeiten kann man mit Jnario zusätzlich Features und Specs zu sogenannten Suites aggregieren und diese dann gemeinsam ausführen. Außerdem bietet Jnario einen Dokumentationsgenerator an, mit dem man Specs in HTML-Dokumente überführen kann. Diese HTML-Dokumente stellen dann den Inhalt der Spezifikation noch übersichtlicher dar. Technologisch basiert Jnario auf Xtend [10] und einem Editor, der mit Xtext [11] erstellt wurde. Dieser ist auch in Abbildung 3 zu sehen.

Listing 3

1. package de.devboost.atdd.jnario

2.
3. import org.hedl.examples.airline.custom.*
4. import org.hedl.examples.airline.entities.*
5. import static extension org.jnario.lib.Should.*
6.
7. Feature: BookFlight
8.
9.   Scenario: Book a seat for a passenger
10.     private AirlineDAO dao = new AirlineDAO(getClass());
11.     private Passenger passenger;
12.     private Flight flight;
13.     private boolean success;
14.
15.     Given a passenger Max Mustermann
16.       passenger = dao.createPassenger(„Max“, „Mustermann“);
17.     And a flight LH-1234 executed on a Boeing 737
18.       var closure = [IDBOperations command |
19.         flight = command.createFlight(„LH-1234“);
20.         flight.setAirplane(command.createAirplaneType(„Boeing 737“, 137));
21.       ];
22.       dao.executeInTransaction(closure);
23.
24.     When a seat is booked
25.       success = dao.bookSeat(passenger, flight);
26.
27.     Then a valid ticket is issued
28.       success => true

NatSpec

Um die Liste der verfügbaren Werkzeuge abzurunden, wollen wir zuletzt noch einen Blick auf das Tool NatSpec werfen [6]. NatSpec steht für „Natural Specification“ und wurde von der DevBoost GmbH im Rahmen eines Kundenprojekts entwickelt. Dabei war die Maßgabe, dass Testfälle nicht nur natürlichsprachlich festgehalten werden können, sondern auch so flexibel wie irgend möglich gestaltet werden sollten. Die Zielgruppe waren Fachexperten mit sehr eingeschränktem Know-how im Bezug auf Programmierung bzw. Programmiersprachen. Aus dieser Anforderung heraus geht NatSpec teilweise andere Wege als Cucumber oder auch Jnario, obwohl die Zielstellung aller Werkzeuge dieselbe ist.

NatSpec nutzt ähnlich wie Cucumber rein natürlichsprachliche Texte zur Spezifikation von Testfällen. Allerdings müssen diese Testfälle keiner vorgegebenen Grundstruktur folgen. Vielmehr kann beliebiger Text zur Definition von Szenarien verwendet werden. Vom Werkzeug selbst werden also weder feste Schlüsselworte noch andere Konventionen vorgegeben.

Der Support-Code wird, wieder ähnlich wie bei Cucumber, in speziellen Test Support Klassen abgelegt. Eine Einbettung in die Szenariobeschreibungen, wie bei Jnario, hätte die Nutzer von NatSpec irritiert, da diese weder Java noch Xtend beherrschten. NatSpec nutzt wie Cucumber Annotationen, um für Support Methoden das zugehörige Muster fest zu legen. Allerdings wird hier nicht auf reguläre Ausdrücke gesetzt, sondern auf eine einfachere Sprache, in der Platzhalter für die Methodenparameter lediglich durch ein Hashtag und den Index des Parameters repräsentiert werden. Der NatSpec Support-Code für unser Beispiel ist in Listing 4 zu finden, die Spezifikation für das Testszenario zeigt Abbildung 4.

Durch die Nutzung der Annotation @TextSyntax wird es möglich, Texte noch flexibler als bei Cucumber auf Testcode abzubilden. Die speziellen Zeichen für Zeilenanfang (^) und Zeilenende ($) entfallen gänzlich. NatSpec trifft hier die Annahme, dass Muster immer zeilenweise erkannt werden. Sollte das einmal nicht der Fall sein, so lässt sich diese Ausnahme von der Regel natürlich auch spezifizieren.

Weiterhin fällt bei genauer Betrachtung von Listing 4 auf, dass manche @TextSyntax-Annotationen nicht alle Parameter benutzen. So taucht beispielsweise bei setSeats() der zweite Parameter (#2) auf, aber der erste nicht. Hier kommt eine weitere Annahme von NatSpec zum Tragen, nämlich dass alle fehlenden Parameter aus dem Kontext der Spezifikation ergänzt werden. Wurde beispielsweise durch einen vorhergehenden Schritt (siehe Abbildung 4) ein Flugzeugtyp erzeugt, so wird dieser automatisch als Argument genutzt. Natürlich zeigt der NatSpec-Editor einen Fehler an, falls kein solches implizites Objekt vorhanden ist.

Durch die Nutzung der impliziten Parameter und der vollständig freien Syntax können Testfälle noch direkter mit natürlicher Sprache definiert werden. Die Autoren der Tests müssen sich nicht mehr an vom Werkzeug vorgegebene Strukturen halten, sondern können die Szenarien so formulieren, wie es für das Projekt am sinnvollsten ist. Wie Abbildung 4 zeigt, entstehen auf diese Weise sehr kompakte Texte. Insbesondere der automatische Bezug auf bereits erzeugte Objekte kommt dem menschlichen Denken sehr nahe, da man Aussagen im Normalfall immer auf den aktuellen Kontext, d. h. das zuletzt Gehörte oder Gelesene bezieht.

Um die Spezifikation von Akzeptanztests für Nutzer noch einfacher zu gestalten, unterstützt NatSpec Synonyme. So kann für jedes Wort eine Liste von Begriffen mit der gleichen Bedeutung festgelegt werden. Beim Abgleich der Muster und der Testanweisungen werden die Synonyme beachtet, so dass Testautoren sich nicht starr an die definierten Muster halten müssen. Diese Funktion ist u. a. bei der Verwendung von Substantiven in der Einzahl und Mehrzahl nützlich.

Der NatSpec-Editor prüft bei der Eingabe immer, ob für den Text passende Muster, das heißt Test-Support-Methoden, im Kontext des Eclipse-Projekts vorhanden sind. Wird ein passendes Muster gefunden, so werden die Worte im Satz entsprechend hervorgehoben. Die Schlüsselworte für die Testsyntax sind daher vollkommen dynamisch, die gewohnten Editorfunktionalitäten wie Syntax Highlighting, Code Completion, Errors and Warnings stehen aber dennoch zur Verfügung. In Abbildung 4 kann man gut erkennen, welche Worte als Schlüsselworte bzw. als Argumente erkannt wurden.

Über die beschriebene Basisfunktionalität hinaus kann NatSpec eine Dokumentation der verfügbaren Satzmuster generieren, die Testautoren die Einarbeitung erleichtert. Zudem existiert eine API, um programmatisch Satzmuster zu erstellen. In dem oben genannten Kundenprojekt wurden auf diese Weise Muster für eine komplette JPA-basierte Persistenzschicht erstellt. So konnte der SupportCode für diese Schicht erzeugt werden, ohne mit großem Aufwand manuell Test-SupportKlassen zu schreiben.

Technologisch basiert NatSpec auf EMFText [12]. Als Sprache für den Test-Support-Code kommt normales Java zum Einsatz. Natürlich kann jede kompatible Sprache (z. B. Xtend) hier auch genutzt werden. NatSpec generiert Tests aus den Testszenarien, das heißt, es wird im Gegensatz zu Cucumber kein Interpreter genutzt. Dementsprechend steht auch bei NatSpec Unterstützung für die gängigen Build-Systeme bereit.

Eine Übersicht der drei vorgestellten Werkzeuge ist in Tabelle 1 zu finden.

Listing 4

1. public class TestSupport {
2.
3.   private AirlineDAO dao;
4.
5.   public TestSupport(AirlineDAO dao) {
6.     super();
7.     this.dao = dao;
8.   }
9.
10.   @TextSyntax(„Assert success“)
11.   public void assertSuccess(boolean actual) {
12.     Assert.assertEquals(true, actual);
13.   }
14.
15.   @TextSyntax(„Create airplane #1“)
16.   public AirplaneType createAirplane(String name) {
17.     return dao.createAirplaneType(name, 0);
18.   }
19.
20.   @TextSyntax(„Assume #2 seats“)
21.   public AirplaneType setSeats(final AirplaneType plane, final int seats) {
22.     dao.executeInTransaction(new ICommand() {
23.
24.       @Override
25.       public void execute(IDBOperations operations) {
26.         AirplaneType planeEntity = operations.getAirplaneType(plane.getId());
27.         planeEntity.setTotalSeats(seats);
28.       }
29.     });
30.     return plane;
31.   }
32.
33.   @TextSyntax(„Create flight #1“)
34.   public Flight createFlight(final String name, final AirplaneType plane) {
35.     final Flight[] result = new Flight[1];
36.     dao.executeInTransaction(new ICommand() {
37.
38.       @Override
39.       public void execute(IDBOperations operations) {
40.         Flight flight = operations.createFlight(name);
41.         flight.setAirplane(operations.getAirplaneType(plane.getId()));
42.         result[0] = flight;
43.       }
44.     });
45.     return result[0];
46.   }
47.
48.   @TextSyntax(„Book seat for #1 #2“)
49.   public boolean bookSeat(String firstname, String lastname, Flight flight) {
50.     Passenger passenger = dao.createPassenger(firstname, lastname);
51.     return dao.bookSeat(passenger, flight);
52.   }
53. }

Vorgehen und Werkzeuge sind da, was gilt es noch zu beachten?

Nachdem die Ansätze verschiedener Werkzeuge zur Unterstützung von ATDD genauer betrachtet wurden, wollen wir noch einmal kurz auf die Implikationen, die ein solches Vorgehen mit sich bringt, eingehen. Zum Ersten rückt der Fachexperte bei ATDD natürlich mehr in den Vordergrund. Das wird möglich, weil die Szenarien für diesen einfacher zu lesen und zu verstehen sind. Infolgedessen, kann der Fachexperte nun aktiv partizipieren und zusammen mit den Entwicklern und Testern die Szenarien erarbeiten. Das ist nicht überraschend, denn es war ja das Ziel der Übung. Allerdings ist es wichtig, sich diesen Fakt vor Augen zu halten, denn eine solche Zusammenarbeit erfordert natürlich auch organisatorische Voraussetzungen. Gelingt das Zusammenspiel, so wird die berüchtigte Kommunikationslücke zwischen den Parteien drastisch reduziert.

Weiterhin fällt auf, dass die Testszenarien, zumindest bei Cucumber und NatSpec, frei von technischen Details sind. Sie machen keinerlei Annahmen über die technische Umsetzung der Anwendung. Das ist auch gut so, weil die Anwendung zum Zeitpunkt der Szenariodefinition unter Umständen noch gar nicht existiert und die Szenarien nicht geändert werden müssen, wenn sich die Anwendung weiter entwickelt. Natürlich muss im letzteren Fall der Test-Support-Code angepasst werden, aber immerhin bleiben die Anforderungen unverändert.

Über die Generierung von ausführbaren Tests hinaus erhält man bei bestimmten ATDDWerkzeugen auch wertvolle Dokumentation. Dies betrifft sowohl Dokumentation für einzelne Szenarien als auch die Dokumentation für die verfügbaren Schritte zum Aufbau neuer Szenarien. Da diese Dokumentation automatisch generiert wird und somit immer aktuell ist, bekommt man wertvolle Artefakte geschenkt.

Betrachtet man das Vorgehen und die Werkzeuge bei ATDD in einem breiteren Blickwinkel, so fällt schnell auf, dass die Spezifikation von Akzeptanztests nur ein Anwendungsfall ist. Die generelle Idee, natürlichsprachliche Texte auf Code abzubilden und diesen auszuführen, eröffnet auch noch andere Möglichkeiten. So lässt sich beispielsweise das Domänenmodell einer Anwendung (z. B. die Entitäten und deren Beziehungen) auf die gleiche Art und Weise beschreiben. Dies ermöglicht auch hier Fachexperten einen einfacheren Zugang zu dem, was die Entwickler tatsächlich produzieren. Letztendlich ist jedes Programm auf diese Art und Weise natürlichsprachlich spezifizierbar. Für Akzeptanztests liegt der Vorteil auf der Hand. Welche anderen Anwendungsfälle ebenso sinnvoll sind, wird sich noch zeigen müssen.

Fazit

Acceptance Test-Driven Development ist ein pragmatischer Ansatz, um die Spezifikation von Anforderungen verständlicher und wartbarer zu gestalten, aber gleichzeitig ausführbare Tests zu erhalten. Durch die Nutzung von natürlicher Sprache können Fachexperten in die Spezifikation mit einbezogen werden. Die Ergebnisse von Workshops zur Anforderungsanalyse können direkt in die Entwicklung mit einfließen. Die Lücke zwischen einer Anforderung, wie sie ein Kunde formuliert, und einem Test, der diese Anforderung automatisiert überprüft, wird dadurch drastisch reduziert. ATDD ist damit ein wichtiger nächster Schritt hin zum effizienteren Testen von Anwendungen.

Links & Literatur

[1] http://www.junit.org
[2] http://www.testng.org
[3] Gojko Adzic: Bridging the Communication Gap: Specification by Example and Agile Acceptance Testing. Neuri (2009)
[4] https://cucumber.io
[5] http://jnario.org
[6] http://www.nat-spec.com
[7] https://github.com/mrpotes/
[8] https://github.com/rlogiacco/Natural
[9] https://github.com/matthewpietal/ Eclipse-Plugin-for-Cucumber
[10] http://www.eclipse.org/xtend/
[11] http://www.eclipse.org/Xtext/
[12] http://www.emftext.org

Schreibe einen Kommentar

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

Kategorien

Recent Posts