Partner von:

Verhaltensgetriebene Software-Entwicklung

Arbeitsplatz [Quelle: pixabay.com, Autor: Picography]

Quelle: pixabay.com, Picography

Behavior Driven Development ist eine praktische Alternative zur testgetriebenen Software-Entwicklung - vorausgesetzt, man hat ein entwicklerfreundliches Framework. JGiven zum Beispiel.

Es gibt in der Software-Entwicklung eine einfache Regel: Je später ein Softwarefehler gefunden wird, desto teurer wird er. Die Explosion der ersten Ariane 5 wurde durch einen Softwarefehler ausgelöst und verursachte einen Schaden von 370 Millionen US-Dollar. Der Fehler ist leider erst nach dem Start der Rakete aufgetreten. Wäre der Fehler noch während der Entwicklung gefunden worden, hätte er mit geringem Aufwand behoben werden können. Es gibt viele Möglichkeiten, Fehler noch während der Entwicklung von Software zu finden. Das wichtigste Mittel dabei ist das Testen.

Testgetriebene Softwareentwicklung

In klassischen Software-Entwicklungsprozessen wird die Software erst nach deren Entwicklung in einer gesonderten Testphase durch Tester manuell getestet. Dieser Prozess ist allerdings sehr teuer und zeitaufwendig, da Fehler, die in der Testphase gefunden werden, eine rückwirkende Änderung der ja eigentlich schon fertigen Software bedeuten. In der sogenannten testgetriebenen Software-Entwicklung sind Tests hingegen ein fester Bestandteil der Entwicklung und nicht in eine separate Phase ausgelagert. Dadurch können Fehler wesentlich früher entdeckt und schneller behoben werden. Der wichtigste Punkt dabei ist, dass es sich nicht um manuell ausgeführte Tests handelt, sondern um automatisierte Tests, die in der Regel von Continuous-Integration-Servern (CI-Servern) kontinuierlich ausgeführt werden. Die testgetriebene Entwicklung hat jedoch einen Nachteil: Die Software-Entwickler testen die von ihnen geschriebene Software selbst. Die Entwickler sind aber oft nicht diejenigen, die die Anforderungen an die Software stellen. Dies kann dazu führen, dass die Software zwar alle Tests erfolgreich absolviert, sie sich aber trotzdem nicht so verhält, wie eigentlich vom Fachexperten gewünscht. Die Fachexperten können diesen Fehler in der Regel allerdings erst bemerken, wenn sie die Software benutzen, also wenn die Entwicklung des jeweiligen Features schon abgeschlossen ist. Da die Tests selbst in einer Programmiersprache wie zum Beispiel Java geschrieben sind, ist es für Fachexperten ohne Programmierkenntnisse unmöglich, die Tests vorher auf ihre Korrektheit zu überprüfen.

Verhaltensgetriebene Software-Entwicklung

Genau an der Stelle setzt die sogenannte verhaltensgetriebene Software-Entwicklung (engl. Behavior Driven Development, BDD) an. Sie ist eine Weiterentwicklung der testgetriebenen Entwicklung, die die Fachexperten in den Software-Entwicklungsprozess besser einbindet. Bei der verhaltensgetriebenen Software-Entwicklung werden Tests nicht alleine von Software-Entwickleren entworfen, sondern in enger Zusammenarbeit mit den jeweiligen Fachexperten. Damit Fachexperten und Software-Entwickler gemeinsam das Verhalten der Anwendung beschreiben können, muss eine einheitliche, von allen Seiten verstandenen, Sprache und Notation verwendet werden. Als Sprache wird dabei immer die Fachsprache verwendet, die auch die Fachexperten selbst verwenden. Jeder Begriff der Fachsprache sollte dabei genau definiert und eindeutig sein. So sollten zum Beispiel keine Synonyme verwendet werden. Also Notation hat sich die Given-When-Then-Notation in der Praxis bewährt. Dabei wird ein bestimmtes Verhalten der Software anhand eines konkreten Beispiels in einem sogenannten Szenario beschrieben, das aus drei Teilen besteht.

Im ersten Teil wird die Ausgangssituation bzw. die Vorbedingung beschriebenen (Given). Der zweite Teil beschreibt die Aktion, die in dem Softwaresystem ausgelöst wird (When). Im letzten Teil werden dann die gewünschten Ergebnisse und Nachbedingungen beschrieben (Then). Als Beispiel wollen wir das Verhalten eines einfachen Kaffeeautomaten beschreiben. Wir formulieren hierzu folgendes Szenario:

Quelle: TNG

Man erkennt schnell, dass ein Szenario eine klar definierte Struktur hat. Dies ist wichtig, damit das Szenario später automatisiert ausgeführt werden kann. Neben einer kurzen Zusammenfassung besteht ein Szenario dabei immer aus mehreren ‘Schritten’. Jeder Schritt wird mit einem vordefinierten Einleitungswort begonnen. Dadurch ist klar, ob es sich um eine Vorbedingung (angenommen), eine Aktion (wenn) oder eine Nachbedingung (dann) handelt. Das Wort ‘und’ kann verwendet werden, um zusätzliche Schritte zu definieren. Das obige Szenario beschreibt den Normalfall, das heißt alle Vorbedingungen zum Produzieren eines Kaffees sind erfüllt. Interessanter sind oft allerdings die Rand- bzw. Fehlerfälle (engl. corner cases), da es hier öfter zu Missverständnissen kommen kann. Was soll zum Beispiel passieren, wenn keine Kapsel im Automat ist? Soll er dann einfach nur heißes Wasser ausgeben oder den Dienst ganz verweigern? Im letzteren Fall müsste der Automat einen Erkennungsmechanismus für die Anwesenheit einer Kapsel besitzen. Oder was soll passieren, wenn der Wassertank nur 100 ml (oder genau 200 ml) Wasser enthält? All diese Sonderfälle sollten durch entsprechende Szenarien abgedeckt werden.

Automatisiertes Ausführen von Szenarien

Das gemeinsame Formulieren von Test-Szenarien ist sehr hilfreich, um im Vorhinein Missverständnisse zwischen Fachexperte und Software-Entwickler zu vermeiden. Software wird allerdings ständig angepasst und weiterentwickelt. Daher ist es wichtig, dass die einmal formulierten Szenarien regelmäßig gegen die Software getestet werden. Da ein manuelles Überprüfen, wie oben schon beschrieben, zu zeitaufwendig ist, müssen die Szenarien automatisiert ausführbar sein. Bei jeder Softwareänderung können dann alle Szenarien durch einen CI-Server automatisch getestet werden. Dadurch wird garantiert, dass sich die Software weiterhin wie spezifiziert verhält. Man spricht in diesem Zusammenhang auch von ‘lebender’ Dokumentation, also Dokumentation, die nicht veraltet.

Damit Szenarien automatisiert ausgeführt werden können, müssen sie an ausführbaren Code gebunden werden. Zu diesem Zweck verwendet man in der Regel ein Framework, das dies übernimmt. Eines der bekanntesten Frameworks in diesem Umfeld ist Cucumber. In Cucumber werden Szenarien in sogenannten Feature-Dateien geschrieben. Dies sind einfache Text-Dateien, in denen in der Regel ein ganzes Feature anhand mehrerer Szenarien beschrieben wird. Um Szenarien nun ausführen zu können, muss für jeden Schritt des Szenarios eine entsprechende Implementierung angeben werden. In Java schreibt man hierzu eine oder mehrere Klassen, die in etwa so aussehen:

Quelle: TNG

Für jeden Schritt in einem Szenario muss genau eine Methode existieren, die genau definiert, was bei der Ausführung des Schrittes passieren soll. Diese Methoden werden nun von Cucumber ausgeführt, wenn der jeweilige Schritt an der Reihe ist. Damit Cucumber weiß, welche Methode zu welchem Schritt gehört, müssen die Methoden mit Annotationen versehen werden. Für jede Schrittart gibt es dabei eine Annotation: @Given, @When und @Then. Mit der Annotation definiert man einen regulären Ausdruck. Eine Schritt-Methode wird genau dann von Cucumber ausgeführt, wenn das Muster, das der reguläre Ausdruckbeschreibt, auf den Text des Schrittes passt. Wie man an der zweiten Methode sieht, können Schritt-Methoden auch Parameter haben. Diese werden im regulären Ausdruck durch Gruppen mittels runder Klammern ausgedrückt.

Nachteile von Cucumber in der Praxis

Auf den ersten Blick macht Cucumber einen guten Eindruck. Erste kleine Beispiele lassen sich in der Regel auch schnell schreiben. Sobald allerdings die Zahl der Szenarien größer wird, erhöht sich mehr und mehr der Aufwand, die existierenden Szenarien zu warten und neue Szenarien zu erstellen. Der Aufwand entsteht durch zwei wesentliche Merkmale von Cucumber: Erstens müssen die Szenarien in einfachen Text-Dateien geschrieben werden und zweitens müssen passende reguläre Ausdrücke gefunden werden. Dadurch, dass Szenarien als Text-Dateien geschrieben werden müssen, sind Programmierer extrem eingeschränkt. Es können zum Beispiel keine Abstraktionsmechanismen verwendet werden, um z.B. Duplikation in Szenarien zu vermeiden. Sämtliche Daten eines Szenarios müssen dazu noch in der Regel als Text hartcodiert werden. Schlussendlich können auch nicht die gewohnten Features wie zum Beispiel Refaktorisierungswerkzeuge verwendet werden, die die Entwicklungsumgebung normalerweise bereitstellt.

Reguläre Ausdrücke haben das Problem, dass bei steigender Zahl auch die Wahrscheinlichkeit steigt, dass zwei reguläre Ausdrücke auf den gleichen Schritt passen. Insbesondere wenn man, um Duplizierung zu vermeiden, die Schritte stark parametrisiert. Reguläre Ausdrücke können oft auch recht kompliziert werden, was den Erstellungs- und Wartungsaufwand zusätzlich erhöht.

Wie oben schon beschrieben, sind die Erstellungs- und Wartungskosten von automatisierten Tests sehr hoch. Wenn diese Aufwände nun durch zusätzliche Hürden eines Frameworks weiter steigen, übersteigen diese meist den für den Kunden akzeptablen Rahmen. Selbst wenn die Kosten vom Kunden akzeptiert werden, sind es häufig die Software-Entwickler selbst, die das Framework wegen des hohen Zusatzaufwandes ablehnen. Dies kann dann dazu führen, dass das Framework nicht oder nur selten verwendet wird und stattdessen weiter normale Tests geschrieben werden, mit den schon oben erwähnten Nachteilen.

JGiven

In einem großen Java-Projekt wollten wir als TNG verhaltensgetriebene Software-Entwicklung einführen. Wir stellten recht schnell fest, dass Cucumber für uns wegen der besagten Nachteile nicht infrage kommt. Alle anderen Frameworks im Java-Umfeld hatten ähnliche Nachteile, benötigten teilweise andere Programmiersprachen wie zum Beispiel Groovy oder Scala oder hatten nicht die für uns notwendigen Features. Wir entschieden uns daraufhin ein eigenes Framework zu schreiben. Herausgekommen dabei ist JGiven: ein frei verfügbares, entwicklerfreundliches Open-Source-Framework zur verhaltensgetriebenen Softwareentwicklung in Java.

Ziele

Folgende Ziele wollten wir mit dem neuen Framework erreichen:

  • Es soll entwicklerfreundlich sein, das heißt Entwickler sollten es gerne benutzen und es nicht wegen eines erhöhten Aufwandes ablehnen
  • Die bekannte Given-When-Then-Notation von typischen BDD-Szenarien sollte beibehalten werden
  • Test-Code soll einfach wiederverwendet werden können, um Test-Code-Duplizierung zu vermeiden
  • Fachexperten sollen die Szenarien ohne Programmierkenntnisse lesen können
  • Es muss keine Programmiersprache außer Java benötigt werden

Ein erstes Szenario in JGiven

In JGiven werden Szenarien direkt als Java-Code geschrieben. Das Beispiel von oben sieht in JGiven
folgendermaßen aus:

Quelle: TNG

Das Beispiel zeigt eine JUnit-Testmethode, erkennbar an der @Test-Annotation, in der das JGiven-Szenario definiert ist. Neben JUnit kann auch TestNG für JGiven verwendet werden. Der Methodenname selbst ist die Beschreibung des Szenarios in Snake_Case-Schreibweise. Innerhalb der Testmethode werden dann die einzelnen Schritte des Szenarios in der Gegeben-Wenn-Dann-Notation geschrieben, jeweils wieder in Snake_Case.

Snake_Case

Für Java-Entwickler etwas ungewöhnlich und streng genommen auch gegen die Java-Code-Konventionen ist die Verwendung von Snake_Case in Methodennamen. Snake_Case ist essentiell für JGiven, da dadurch die korrekte Groß-Kleinschreibung verwendet werden kann, was entscheidend für die Lesbarkeit der generierten Berichte ist. In der Praxis hat sich die parallele Verwendung von CamelCase für normalen Java-Code und Snake_Case für Test-Szenarien als problemlos herausgestellt. Falls die Verwendung von Snake_Case wegen Projekt-Vorgaben unmöglich sein sollte, kann die korrekte Schreibweise mit der @As-Annotation angegeben werden. Dies ist auch nützlich, wenn Sonderzeichen im Bericht erscheinen sollen, die nicht in Java-Methodennamen erlaubt sind. Es kann auch CamelCase verwendet werden, wodurch allerdings die Groß- und Kleinschreibung von JGiven nicht immer korrekt erkannt werden kann.

Stage-Klassen

Wie in Cucumber ist es natürlich auch in JGiven nötig, die Implementierung der Schritte des Szenarios zu definieren. Die Klassen, in denen die Schritte definiert sind, heißen in JGiven Stage-Klassen. Ein Szenario in JGiven besteht in der Regel aus drei Stage-Klassen. Entsprechend der Gegeben-Wenn-Dann-Notation gibt es eine Klasse für die Gegeben-Schritte, eine für die Wenn-Schritte und eine für die Dann-Schritte. Diese Modularisierung der Szenarien hat insbesondere für die Wiederverwendung von Test-Code große Vorteile. Szenarien können so nach dem Baukastenprinzip aus den Stage-Klassen zusammengesetzt werden. Bewährt hat sich auch die Verwendung von Vererbung innerhalb der Stage-Klassen, bei denen speziellere, seltener gebrauchte Stages von allgemeineren, öfter gebrauchten Stages erben. Ein Szenario ist nicht auf drei Stage-Klassen beschränkt, sondern kann aus beliebig vielen Stages bestehen. Zusätzliche Stages können einfach mit der @ScenarioStage-Annotation in die Test-Klasse injiziert werden. Für das Beispiel benötigen wir drei Stage-Klassen: GegebenKaffeeautomat, WennKaffeeautomat und DannKaffeeautomat. Damit die Stage-Klassen in unserem Test verwendet werden können, muss die Test-Klasse von der von JGiven bereitgestellten SzenarioTest-Klasse erben und dessen Typ-Parameter entsprechend setzen:

Quelle: TNG

Wie sehen nun die Implementierungen der Stage-Klassen aus? Dazu schauen wir uns die GegebenKaffeAutomat-Klasse näher an:

Quelle: TNG

Das Beispiel zeigt mehrere wichtige Konzepte. Das Erste ist, dass Stage-Klassen in der Regel von einer von JGiven vordefinierten Klasse erben, in unserem Fall der Stufe-Klasse (in englischen Szenarien entspricht das der Stage-Klasse). Des Weiteren folgen Stage-Klassen dem Fluent-Interface-Pattern, d.h. jede Methode gibt als Rückgabewert das aufgerufene Objekt wieder zurück. Damit das Fluent-Interface-Pattern auch unter Vererbung korrekt funktioniert, wird außerdem ein Typ-Parameter an die Super-Klasse durchgereicht, der dem eigenen Typ der Klasse entspricht. Die self()-Methode liefert nun diesen Typ wieder zurück. Was die Annotation @ProvidedScenarioState bedeutet, wird nun im Folgenden erläutert.

Zustandstransfer

Ein typisches Szenario läuft in der Regel folgendermaßen ab: In der Gegeben-Stage wird ein bestimmter Zustand hergestellt. Auf diesen Zustand wird dann in der Wenn-Stage zugegriffen, eine Aktion ausgeführt und ein Ergebniszustand produziert. Schließlich wird in der Dann-Stage der Ergebniszustand ausgewertet. Um nun Zustände zwischen Stages zu transportieren, hat JGiven einen Mechanismus, der automatisch Felder von Stage-Klassen ausliest und in die folgende Stage schreibt. Felder, die mit @ScenarioState, @ProvidedScenarioState oder @ExpectedScenarioState annotiert werden, werden dabei berücksichtigt. Man kann den Mechanismus mit Dependency-Injection vergleichen, mit dem Unterschied, dass Werte nicht nur injiziert, sondern auch extrahiert werden. In unserem Beispiel wollen wir die Kaffeeautomat-Instanz für die folgenden Stages verfügbar machen und annotieren deswegen das entsprechende Feld mit @ProvidedScenarioState. Nachfolgende Stages können sich nun die Kaffeeautomat-Instanz injizieren lassen, indem sie ein entsprechendes Feld deklarieren und es mit @ExpectedScenarioState annotieren. Der Zustandstransfermechanismus von JGiven ist entscheidend für die Modularität. Dadurch, dass eine Stage nur deklariert welchen Zustand sie benötigt und nicht von welcher Stage der Zustand kommen muss, können beliebige Stages miteinander kombiniert werden, solange die jeweilig benötigten Zustände von den vorherigen Stages bereitgestellt werden.

Tags

Dem aufmerksamen Leser ist sicher aufgefallen, dass der Beispieltest mit @Story("COFFEE-1") annotiert ist. Dies ist keine vordefinierte JGiven-Annotation, sondern eine für das Beispiel definierte Annotation. Die @Story-Annotation ist selbst wiederum mit @IsTag annotiert, wodurch JGiven es als Tag erkennt. Tags erscheinen im generierten HTML-Bericht und ermöglichen es, Szenarien nach beliebigen Kriterien zu gruppieren, unabhängig von der Test-Klasse, in der sie definiert sind.

Berichte für Fachexperten

Mit dem, was bisher beschrieben wurde, sind schon vier der fünf oben genannten Ziele erreicht. JGiven ist entwicklerfreundlich, da Szenarien direkt in Java-Code geschrieben werden. Szenarien werden in der Given-When-Then-Notation geschrieben und Stage-Klassen sorgen für eine sehr gute Code-Wiederverwendbarkeit. Es ist auch keine weitere Programmiersprache außer Java nötig, um JGiven zu verwenden. Falls wir an dieser Stelle aufgehört hätten an JGiven zu arbeiten, wäre JGiven allerdings nur ein nettes Test-Tool für Java-Entwickler. Das entscheidende Merkmal eines echten BDD-Tools würde allerdings fehlen: die Zusammenarbeit mit den Fachexperten. Dazu müssen Fachexperten die Szenarien allerdings lesen können und zwar ohne vorher Java lernen zu müssen. Die Lösung für dieses Problem ist denkbar einfach: JGiven generiert während der Ausführung der Szenarien Berichte. Diese können dann von Fachexperten gelesen werden. Das Generieren der Berichte erzeugt dabei keinen zusätzlichen Aufwand für die Entwickler.

Textausgabe

Für schnelles Feedback gibt es die Textausgabe von JGiven. Sie wird automatisch generiert während die Szenarien ausgeführt werden. Für das obige Beispiel generiert JGiven die folgende Textausgabe:

Quelle: TNG

Wie man sieht, ist die Textausgabe praktisch identisch zu dem Text, den man in Cucumber schreiben würde. Sie Textausgabe dient im Wesentlichen den Entwicklern während der Entwicklung des Szenarios. Zusätzlich generiert JGiven noch JSON-Dateien, die dann in einem späteren Schritt in einen HTML-Bericht konvertiert werden können.

HTML-Bericht

Der HTML-Bericht ist ein zentraler Bestandteil von JGiven. Erst der HTML-Bericht ermöglicht es Fachexperten die Szenarien zu lesen und zu beurteilen. Der Bericht ist interaktiv, d.h. man kann nach Inhalten suchen und kann Szenarien nach bestimmten Kriterien filtern und sortieren. Das obige Beispiel sieht im HTML-Report folgendermaßen aus:

JGiven [Quelle: TNG Technology Consulting]

Einbindung ins Projekt

JGiven ist, wie bei Java üblich, über Maven-Central verfügbar. JGiven wird entweder mit JUnit oder TestNG zusammen verwendet. Für JUnit fügt man folgende Abhängigkeit zur Maven-Konfiguration hinzu:

Quelle: TNG

Für TestNG heißt die Artefakt-ID jgiven-testng. Um nach der Test-Ausführung den HTML-Bericht zu generieren, bindet man noch das JGiven-Maven-Plugin ein:

Quelle: TNG

Ein mvn verify führt nun die Tests aus und generiert anschließend den JGiven HTML-Bericht.

Erfahrungen mit JGiven aus der Praxis

JGiven wird seit zwei Jahren erfolgreich von TNG eingesetzt. In dem anfangs erwähnten Java-Projekt sind mittlerweile schon über 2.000 Szenarien entstanden, die nach und nach unsere existierenden Tests ersetzen. Das Framework wurde von allen Entwicklern sehr schnell angenommen und auch sehr schnell erlernt. Entwickler, die neu ins Projekt kommen, erlernen das Framework ohne Probleme innerhalb der ersten Wochen. Innerhalb von TNG wird das Framework nun schon in anderen Projekten eingesetzt und auch außerhalb von TNG wird JGiven mittlerweile aktiv eingesetzt, unter anderem in einem Open-Source-Projekt von Siemens.

Zusammenfassung

Hinter verhaltensgetriebener Entwicklung steht die Idee, dass Fachexperten und Entwickler gemeinsam an der Verhaltensspezifikation eines Softwaresystems arbeiten. Dadurch werden Unklarheiten vermieden und Fehler so früh wie möglich aufgedeckt. Existierende Werkzeuge für Java haben allerdings entweder einen erhöhten Wartungsaufwand oder benötigen eine weitere Programmiersprache wie Groovy oder Scala. JGiven vermeidet diese Probleme, in dem Szenarien direkt in Java-Code geschrieben werden. Aus dem Java-Code erzeugt JGiven HTML-Berichte, die von Fachexperten ohne Programmierkenntnisse gelesen werden können. Die generierten HTML-Berichte stellen dabei zusätzlich eine automatisch validierte und somit lebende Dokumentation des Systems dar. JGiven hat noch diverse Features, deren Beschreibung allerdings den Rahmen dieses Artikels sprengen würden. Ausführliche Informationen, Beispiele und auch ein Einführungsvideo sind auf der offiziellen Webseite abrufbar. Das komplette Beispiel aus diesem Artikel ist unter GitHub verfügbar.

Dr. Jan Schäfer ist Senior Consultant und seit vier Jahren bei der TNG Technology Consulting GmbH in München tätig.

nach oben

Stipendiaten und Alumni von e-fellows.net können kostenlos oder ermäßigt zahlreiche Online-Kurse belegen oder an Seminaren teilnehmen.

Verwandte Artikel

Hol dir Karriere-Infos,

Jobs und Events

regelmäßig in dein Postfach

Kommentare (0)

Zum Kommentieren bitte einloggen.

Das könnte dich auch interessieren