Not yet registered? Try Yulup with a free account!
Yulup Logo

Dan North: Einführung in BDD

Einleitung

[Der folgende Artikel wurde von Dan North im Original auf Englisch publiziert. Er schrieb den Post in der Ich-Form, welche auch für die Deutsche Übersetzung beibehalten wird.]

Ich hatte ein Problem: Während ich agile Methoden wie Test-Driven Development (TDD) auf Projekten in verschie­denen Umgebungen anwandte oder auch schulte, traf ich immer wieder auf die gleichen Miss­ver­ständ­nisse und Ver­wechs­lungen. Entwickler wollten wissen, wo sie beginnen sollten, was sie testen und was sie nicht testen sollten, wie sie ihre Tests nennen sollten, und sie wollten ver­stehen, warum ein Test fehl­schlug.

Je mehr ich in TDD ein­tauchte, desto mehr fühlte sich meine Reise nicht wie ein schritt­weises Lernen sondern ein Irren in dunklen Gassen an. Ich erinnere mich, dass ich weit öfters dachte "Wenn mir dies nur jemand im Voraus gesagt hätte!" als "Wow, da hat sich eine Türe aufgetan." Ich kam zum Ent­schluss, dass es möglich sein müsste, TDD auf eine Weise zu präsen­tieren, so dass man direkt die süssen Früchte ernten und die Stol­per­fallen ver­meiden könnte.

Meine Antwort ist Behaviour-Driven Development (BDD). Es hat sich aus bewährten agilen Methoden entwickelt und es zielt darauf ab, diese für Teams, für die agile Software-Entwick­lung noch neu ist, leichter erlern- und einsetz­bar zu machen. Mit der Zeit ist BDD gewachsen und umfasst auch die Bereiche agile Analyse und auto­mati­siertes Acceptance-Testing.

Inhaltsverzeichnis

Sätze als Namen für Test­methoden

Mein erstes Aha-Erlabnis hatte ich, als mir das schlagend einfache Tool TestDox gezeigt wurde, das von meinem Kollegen Chris Stevenson geschrieben wurde. Es nimmt eine JUnit Testklasse und schreibt alle Methodennamen als Sätze. Ein Test-Case, der so aussieht:

public class CustomerLookupTest extends TestCase {
    testFindsCustomerById() {
        ...
    }
    testFailsForDuplicateCustomers() {
        ...
    }
    ...
}
ergibt dann in etwa:

CustomerLookup
- finds customer by id
- fails for duplicate customers
- ...
              

Das Wort "test" wird bei bei den Klassen wie den Methoden gestrichen, und der mit Binnen­Majuskel (CamelCase) geschriebene Name der Methode wird in normalen Text umge­wandelt. Dies ist alles, was es macht, doch der resul­tierende Effekt ist erstaunlich.

Entwickler entdeckten für sich, dass es ihnen so einen Teil des Dokumen­tations­aufwandes abnahm, und so begannen sie, Methoden mit ganzen Sätzen zu benennen. Darüber hinaus fanden sie heraus, dass, wenn sie die Methoden allgemein verständlich benannten, die daraus generierte Dokumen­tation auch für Anwender, Analysten und Tester durchaus Sinn macht.

Inhaltsverzeichnis

Eine einfache Vorlage für einfache Tests

Dann stolperte ich über die Konvention, dass die Namen der Testmethoden mit dem Wort "should" (soll) beginnen sollten. Diese Vorlage — The class should do something / Die Klasse soll etwas tun — bedeutet, dass man nur einen Test für die aktuelle Klasse schreiben kann. So bleibt man fokussiert. Wenn man sich dabei ertappt, einen Test zu schreiben, dessen Name nicht in diese Vorlage passt, dann deutet dies darauf hin, dass dieses "Verhalten" (Behaviour) irgendwo anders hingehört.

Ich war beispielsweise daran, eine Klasse zu schreiben, die Eingabefelder überprüft. Die meisten Felder waren für einfache Kundendaten wie Vor- oder Nachname — aber dann gab es da ein Feld für das Geburtsdatum und eines für das Alter. Ich begann, einen ClientDetailsValidatorTest mit den Methoden testShouldFailForMissingSurname und testShouldFailForMissingTitle zu schreiben.

Dann begann ich das Alter zu berechnen und landete in einem Wirrwarr komplizierter Business-Regeln: Was, wenn sowohl das Geburtsdatum als auch das Alter angegeben wurden, diese aber nicht übereinstimmten? Was, wenn der Geburtstag heute ist? Wie berechne ich das Alter, wenn ich nur den Geburtstag habe? Ich begann, mir mehr und mehr komplizierte Namen von Testmethoden zu notieren, um das Verhalten zu beschreiben — bis ich beschloss, mich etwas anderem zu widmen. Dies liess mich schliesslich eine neue Klasse AgeCalculator mit einem eigenen AgeCalculatorTest einführen. Die gesamte Alters­berechnung verschob ich in diesen Rechner, so dass die überprüfende Klasse nur noch einen Test benötigt um sicherzustellen, dass sie richtig mit dem Alters­rechner interagiert.

Wenn eine Klasse mehr als nur etwas macht, halte ich es für gewöhnlich für ein starkes Indiz, dass ich eine weitere Klasse, die die zusätzlichen Dinge erledigt, einführen sollte. Ich definiere den neuen Service als ein Interface, indem ich beschreibe, was es macht. Und dann übergebe ich diesen Service via den Klassen-Konstruktor:

public class ClientDetailsValidator {
 
    private final AgeCalculator ageCalc;
 
    public ClientDetailsValidator(AgeCalculator ageCalc) {
        this.ageCalc = ageCalc;
    }
}
Diese Art, Objekte miteinander zu verknüpfen — auch als Dependency Injection [Artikel von Martin Fowler, deutschsprachiger Wikipedia-Artikel] bekannt — ist speziell hilfreich im Zusammenhang mit Mocks.

Inhaltsverzeichnis

Ein aussagekräftiger Testname hilft, wenn ein Test fehlschlägt

Nach einer Weile fand ich heraus, dass, wenn ein Test nach einer Code-Änderung fehlschlug, ich einfach den Namen der Testmethoden anschauen konnte, um das beabsichtigte Verhalten des Codes herauszufinden. Typischerweise hatte eines der folgenden drei Dinge stattgefunden:

  • Ich hatte einen Fehler eingebaut. Böser Junge. Lösung: Bug-fixing.
  • Das angestrebte Verhalten war immer noch von Bedeutung, aber es war an einen anderen Ort im Code verschoben worden. Lösung: Verschiebe auch den Test und passe ihn nötigenfalls an.
  • Dieses Verhalten ist nicht mehr korrekt — die Voraussetzungen des Systems haben sich verändert. Lösung: Lösche den Test.
Gerade das Dritte passiert häufig bei agilen Projekten, weil sich das Verständnis bezüglich des Projekts weiterentwickelt hat. Leider haben TDD-Novizen oft eine immanente Angst, Tests zu löschen. Als ob dies irgendwie die Qualität ihres Codes reduzieren würde.

Ein subtilerer Aspekt des Wortes should (soll) wird offensichtlich, wenn man es mit den formaleren Alternativen will oder shall (wird) vergleicht. Das Wort should erlaubt es, die Voraussetzung des Tests zu hinterfragen: "Soll es? Wirklich?" Dies macht es einfacher zu entscheiden, ob ein Test fehlschlug, weil du einen Bug eingebaut hast oder einfach, weil deine früheren Annahmen über das Verhalten des Systems nicht mehr zutreffend sind.

Inhaltsverzeichnis

"Verhalten" ist das bessere Wort als "Test"

Jetzt hatte ich ein Werkzeug (TestDox), um das Wort "Test" zu ent­fernen, und eine Vor­lage für alle Namen der Test­methoden. Plötzlich wurde mir klar, dass die Miss­verständ­nisse bezüglich TDD meist durch das Wort "Test" verursacht wurden.

Damit will ich nicht sagen, dass Testing nicht intrinsisch sei für TDD — der resultierende Satz an Test­methoden ist ein effektiver Weg sicher­zustellen, dass dein Code funktioniert. Aber wenn die Methoden dein System nicht umfassend beschreiben, dann geben sie dir ein falsches Gefühl von Sicherheit.

Ich begann, das Wort "Verhalten" (Behaviour) anstelle von "Test" im Zusammenhang mit TDD zu verwenden und fand heraus, dass es nicht nur gut passte, sondern auch viele Fragen, die mir sonst im Rahmen meiner Schulungen gestellt wurden, auf wundersame Weise verschwanden. Und ich hatte Antworten auf diese Fragen. Wie ein Test benannt werden soll ist einfach — er beschreibt das Verhalten, an welchem du interes­siert bist. Wieviel man testen soll wird ir­rele­vant: so viel, wie man mit einem Satz be­schrieben kann. Wenn ein Test fehlschlägt, gehe einfach den oben be­schriebe­nen Prozess durch: entweder du hast einen Fehler eingebaut, das Verhalten wurde verschoben oder der Test ist nicht mehr relevant.

Ich fand den Wechsel vom Denken in Tests hin zum Denken in Verhalten so tiefschürfend, dass ich begann, TDD als BDD zu bezeichnen: Behaviour-Driven Development.

Inhaltsverzeichnis

JBehave stellt Verhalten über Testing

Ende 2003 beschloss ich, nicht nur davon zu reden, sondern ein Projekt in Angriff zu nehmen. Ich begann JBehave zu schreiben, das ein Ersatz für JUnit werden sollte. Ich entfernte alle Referenzen zu Testing und ersetzte sie mit einem Vokabular rund um das Verifizieren von Verhalten. Ich machte dies um zu sehen, wie sich ein solches Framework entwickelt, wenn ich mich streng an meine neuen behaviour-driven Mantras hielt. Ebenso dachte ich, dass es ein hilf­reiches Unter­richts­werk­zeug sein könnte, um TDD und BDD ohne die Ablenkung durch das Test-basierte Vokabular einführen zu können.

Um das Verhalten einer hypothetischen Klasse CustomerLookup zu definieren, würde ich eine Verhaltens-Klasse schreiben, z.B. CustomerLookupBehavior. Sie würde Methoden enthalten, die mit dem Wort "should" begännen. Das JBehave-Tool würde die Verhaltens-Klasse in­stan­zi­ie­ren und sequenziell die Verhaltens-Methoden aufrufen, so wie es auch JUnit mit den Tests macht. Es würde fortlaufend den Fortschritt dokumentieren und am Schluss eine Zusammenfassung ausgeben.

Mein erster Meilenstein war, dass sich JBehave selbst verifizierte. Ich hatte einfach die Verhalten hinzu­ge­fügt, die es erlaubten, sich selbst zu über­prüfen. Es war mir möglich, alle JUnit-Tests in JBehave-Verhalten zu über­führen und das gleiche Feedback zu erhalten wie mit JUnit.

Inhaltsverzeichnis

Bestimme das wichtigste Verhalten

Dann entdeckte ich das Konzept des Wertes für das 'Business'. Natürlich war ich mir immer bewusst, dass ich aus einem bestimmten Grunde das jeweilige Stück Software schrieb. Aber ich hatte nie wirklich über den Wert der Software, die ich schrieb, nachgedacht. Ein Kollege, der Business-Analyst Chris Matts, hat mich darauf gebracht, über den Wert für das Business im Zusammenhang mit Behaviour-Driven Development nachzudenken.

Mit dem Ziel im Kopf, JBehave selbst-überprüfend zu machen, fand ich einen einfachen Weg, fokussiert zu bleiben. Ich musste mich jeweils nur fragen: Was ist das momentan wichtigste Ding, das das System noch nicht kann?

Diese Frage macht es notwendig, den Wert der noch nicht implementierten Features zu quantifizieren und diese dann entsprechend zu priorisieren. Es hilft dir ebenso, die Verhaltens-Klasse zu benennen: Das System macht im Moment X noch nicht (wobei X für ein sinnvolles Verhalten steht). Und wenn X wichtig ist, d.h. das System sollte X können, dann ist deine nächste Verhaltens-Methode einfach:

public void shouldDoX() {
    // ...
}
Jetzt hatte ich eine Antwort auf eine weitere TDD-Frage, nämlich wo man beginnen soll!

Inhaltsverzeichnis

Anforderungen sind auch Verhalten

Zu diesem Zeitpunkt hatte ich ein Framework, das mir zu verstehen und zu erklären half, wie TDD funktio­niert. Ebenso hatte ich einen Ansatz, der all die Stolperfallen vermied, die ich bisher angetroffen hatte.

Gegen Ende 2004, als ich mein frisch gefundenes, verhaltens-basiertes Vokabular Chris Matts präsen­tierte, sagte dieser "Aber dies ist ja wie Analyse." Dann gab es eine lange Pause, während wir darüber nach­dachten. Und dann entschieden wir, diesen verhaltens-getriebenen Ansatz auf das Definieren von Anfor­de­rungen anzuwenden. Wenn es uns möglich sein sollte, für Analysten, Tester, Entwickler und für's 'Business' ein konsistentes Vokabular zu entwickeln, dann könnten wir erfolgreich einige der Doppeldeutigkeiten und Miss­verständ­nisse aus dem Weg räumen, wenn Techniker mit Leuten aus dem Business sprechen.

Inhaltsverzeichnis

BDD bietet eine "universelle Sprache" für die Analyse

In etwa zu dieser Zeit erschien der Bestseller "Domain-Driven Design" von Eric Evans. Darin beschreibt er das Konzept, ein System mit einer universellen Sprache, die auf dem Geschäfts­umfeld des Projektes basiert, zu modellieren. So fliesst die Sprache des Geschäfts­umfeldes direkt in die Code-Basis ein.

Chris Matts und ich realisierten so, dass wir daran waren, eine universelle Sprache für den Analyse-Prozess zu definieren. Wir hatten einen guten Ausgangs­punkt erreicht: Im Unter­nehmen hatte sich bereits ein Story-Template etabliert, das wie folgt aussah:

Englisch   Deutsch
As a [X]   Als [X]
I want [Y]   möchte ich [Y],
so that [Z].   um [Z].

Wobei Y ein Feature, Z ein Nutzen oder Ergebnis des Features und X die Person (oder Rolle) ist, die vom Nutzen profitieren wird. Die Stärke liegt darin, dass du gezwungen bist, den Wert einer Story zu identifizieren, schon während du sie definierst. Wenn es keinen echten Business-Nutzen für eine Story gibt, dann endet man oft bei etwas wie "...möchte ich [irgendein Feature], um [Hey, ich mach' es einfach, ok?]." Dies kann es viel leichter machen, irgend­welche eso­te­ri­schen Anfor­de­rungen zu entrümpeln.

Von hier ausgehend entdeckten Chris und ich, was jeder agile Tester schon wusste: Das Verhalten einer User Story besteht einfach aus deren Akzeptanz­kriterien — wenn das System alle Akzeptanz­kriterien erfüllt, dann verhält es sich korrekt; wenn es nicht alle erfüllt, dann verhält es sich nicht korrekt. So erstellten wir eine Vorlage, um die Akzeptanz­kriterien einer User Story zu erfassen.

Die Vorlage sollte genügend Freiraum gewähren, so dass sie nicht künstlich oder einengend auf Analysten wirkt, aber ausreichend strukturiert sein, dass wir die User Story in ihre Bestandteile aufbrechen und diese automatisieren konnten. Wir begannen die Akzeptanz­kriterien als Scenarios zu beschreiben, welche die folgende Form hatten:

Angenommen sei ein Ursprungszustand,
wenn ein Ereignis eintritt,
dann stelle ein bestimmtes Ergebnis sicher.

Um dies zu illustrieren, lasse uns das klassische Beispiel des Geldautomaten benutzen. Eine der Story Cards könnte so aussehen:

Kunde hebt Geld ab
Als Kunde
möchte ich Geld vom Automaten beziehen,
um nicht in der Schlange anstehen zu müssen.

Und wie wissen wir nun, wann wir diese User Story fertig gestellt haben? Es gibt verschie­dene Scenarios zu berück­sichtigen: Auf dem Konto ist Geld vorhanden; das Konto ist über die Über­zugs­limite überzogen; das Konto ist über­zogen, aber noch inner­halb der Über­zugs­limite. Natürlich gibt es noch weitere Scenarios wie: Es hat noch Geld auf dem Konto, doch mit dem Bezug würde das Konto über­zogen oder der Geld­automat hat zu wenig Geld.

Die beiden ersten Scenarios könnten mit der Angenommen-wenn-dann-Vorlage so aussehen:

Scenario 1: Auf dem Konto ist Geld vorhanden
Angenommen auf dem Konto ist Geld vorhanden
und die Bankkarte ist gültig
und der Automat enthält Geld
wenn der Kunde Geld abheben möchte
dann belaste das Konto entsprechend
und zahle das Geld aus
und gib die Karte zurück.

Beachte die Verwendung von "und", um mehrere Bedingungen oder Ergeb­nisse auf natür­liche Weise zu verknüpfen.

Scenario 2: Das Konto ist über die Überzugslimite überzogen
Angenommen das Konto ist überzogen
und die Bankkarte ist gültig
wenn der Kunde Geld abheben möchte
dann stelle eine entsprechende Meldung dar
und zahle das Geld nicht aus
und gib die Karte zurück.

Beide Scenarios basieren auf dem gleichen Ereignis und haben sogar einige der Annahmen/Vorbe­dingungen und Ergeb­nisse gemein. Dies wollen wir hervorheben, indem wir die Vorbe­dingungen, Ereig­nisse und Ergeb­nisse möglichst wieder­verwenden.

Inhaltsverzeichnis

Akzeptanz­kriterien sollten ausführbar sein

Die Bestandteile eines Scenarios (Vorbe­dingungen, Ereignisse, Ergebnisse) sind genügend feingranular, um direkt in Programm­code umgesetzt zu werden. JBehave definiert ein Objekt­modell, das es uns erlaubt, die Bestand­teile des Scenarios direkt als Java-Klassen abzubilden.

Man schreibt eine Klasse für jede Vorbe­dingung:

public class AccountIsInCredit implements Given {
    public void setup(World world) {
        ...
    }
}
public class CardIsValid implements Given {
    public void setup(World world) {
        ...
    }
}
und eine für das Ereignis:
public class CustomerRequestsCash implements Event {
    public void occurIn(World world) {
        ...
    }
}
und so weiter für die Ergeb­nisse. Dann verknüpft JBehave alles und führt es aus. Es erzeugt eine "Welt" (World), die nur dazu dient, deine Objekte abzu­speichern, und übergibt sie an jede der Vorbe­dingungen, so dass diese sie mit ihren Zuständen populieren können. Danach lässt JBehave das Ereignis in der Welt "stattfinden" (occur in), was das eigentliche Verhalten des Scenarios ablaufen lässt. Schluss­endlich wird die Kontrolle an die Ergeb­nisse, die wir für die User Story definiert haben, über­geben.

Dass jede Klasse einem Bestand­teil entspricht, erlaubt es uns, Bestand­teile in anderen User Stories oder Scenarios wieder­zu­ver­wenden. Zu Beginn werden die Bestand­teile mit Mocks umgesetzt, z.B. um ein Konto mit Geld zu haben oder eine gültige Bank­karte. Die Mocks bilden den Ausgangs­punkt für die Implemen­tation des Verhaltens. Wenn man die Applika­tion dann umsetzt, werden die Vorbe­dingungen und Ergeb­nisse nach und nach so abge­ändert, dass sie die richtigen Klassen, die du inzwischen implemen­tiert hast, ver­wenden. So entstehen bis zum Zeit­punkt, wenn das Scenario fertig ist, richtige und komplette funktionale Tests.

Inhaltsverzeichnis

Die Gegenwart und die Zukunft von BDD

[Stand: März 2006] Nach einer kurzen Pause wird JBehave nun wieder weiter entwickelt. Die Basis ist ziemlich vollständig und robust. Der nächste Schritt wird die Integration in beliebte Java-IDEs wie IntelliJ IDEA und Eclipse sein.

Dave Astels hat aktiv die Werbe­trommel für BDD gerührt. Sein Blog und mehrere Artikel haben diverse Aktivitäten hervorgerufen. Am bemerkens­wertesten davon ist das Projekt rspec, um ein BDD-Framework in der Sprache Ruby aufzubauen. Ich habe begonnen, an rbehave zu arbeiten. rbehave wird eine Implemen­tation von JBehave in Ruby sein.

Einige meiner Arbeits­kollegen haben BDD-Techniken auf einer Viel­zahl von echten Projekten eingesetzt und schätzen die Methodik als sehr erfolgreich ein. Das JBehave-Ablauf-Tool — der Teil, der die Akzeptanz­kriterien überprüft — wird aktiv weiter­ent­wickelt.

Meine Vision ist ein "360-Grad-Editor", mit welchem Business-Analysten und Tester in der Lage sind, User Stories in einem gewöhnlichen Text-Editor zu erfassen. Dieser erstellt dann die Rümpfe für die Verhaltens-Klassen — alles in der Sprache ihres Geschäfts­umfeldes. Behaviour-Driven Development hat sich durch die Hilfe vieler Personen weiter­ent­wickelt, und ich bin ihnen allen sehr dankbar!

Inhaltsverzeichnis
      Yulup

Made with in Zurich:

Yulup
Stockerstrasse 32
8002 Zurich
Switzerland

info@yulup.com


© YEAR Wyona | Contact | Twitter

Yulup Logo