Sätze als Namen für Testmethoden
Eine einfache Vorlage für einfache Tests
Ein aussagekräftiger Testname hilft, wenn ein Test fehlschlägt
"Verhalten" ist das bessere Wort als "Test"
JBehave stellt Verhalten über Testing
Bestimme das wichtigste Verhalten
Anforderungen sind auch Verhalten
BDD bietet eine "universelle Sprache" für die Analyse
[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 verschiedenen Umgebungen anwandte oder auch schulte, traf ich immer wieder auf die gleichen Missverständnisse und Verwechslungen. 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 verstehen, warum ein Test fehlschlug.
Je mehr ich in TDD eintauchte, desto mehr fühlte sich meine Reise nicht wie ein schrittweises 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 Entschluss, dass es möglich sein müsste, TDD auf eine Weise zu präsentieren, so dass man direkt die süssen Früchte ernten und die Stolperfallen vermeiden 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-Entwicklung noch neu ist, leichter erlern- und einsetzbar zu machen. Mit der Zeit ist BDD gewachsen und umfasst auch die Bereiche agile Analyse und automatisiertes Acceptance-Testing.
InhaltsverzeichnisMein 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 BinnenMajuskel (CamelCase) geschriebene Name der Methode wird in normalen Text umgewandelt. Dies ist alles, was es macht, doch der resultierende Effekt ist erstaunlich.
Entwickler entdeckten für sich, dass es ihnen so einen Teil des Dokumentationsaufwandes 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 Dokumentation auch für Anwender, Analysten und Tester durchaus Sinn macht.
InhaltsverzeichnisDann 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 Altersberechnung verschob ich in diesen Rechner, so dass die überprüfende Klasse nur noch einen Test benötigt um sicherzustellen, dass sie richtig mit dem Altersrechner 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
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:
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.
InhaltsverzeichnisJetzt hatte ich ein Werkzeug (TestDox), um das Wort "Test" zu entfernen, und eine Vorlage für alle Namen der Testmethoden. Plötzlich wurde mir klar, dass die Missverständnisse 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 Testmethoden ist ein effektiver Weg sicherzustellen, 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 interessiert bist. Wieviel man testen soll wird irrelevant: so viel, wie man mit einem Satz beschrieben kann. Wenn ein Test fehlschlägt, gehe einfach den oben beschriebenen 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.
InhaltsverzeichnisEnde 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 hilfreiches Unterrichtswerkzeug 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 instanziieren 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 hinzugefügt, die es erlaubten, sich selbst zu überprüfen. Es war mir möglich, alle JUnit-Tests in JBehave-Verhalten zu überführen und das gleiche Feedback zu erhalten wie mit JUnit.
InhaltsverzeichnisDann 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
Zu diesem Zeitpunkt hatte ich ein Framework, das mir zu verstehen und zu erklären half, wie TDD funktioniert. 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äsentierte, sagte dieser "Aber dies ist ja wie Analyse." Dann gab es eine lange Pause, während wir darüber nachdachten. Und dann entschieden wir, diesen verhaltens-getriebenen Ansatz auf das Definieren von Anforderungen 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 Missverständnisse aus dem Weg räumen, wenn Techniker mit Leuten aus dem Business sprechen.
InhaltsverzeichnisIn 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äftsumfeld des Projektes basiert, zu modellieren. So fliesst die Sprache des Geschäftsumfeldes 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 Ausgangspunkt erreicht: Im Unternehmen 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, irgendwelche esoterischen Anforderungen 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 Akzeptanzkriterien — wenn das System alle Akzeptanzkriterien 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 Akzeptanzkriterien 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 Akzeptanzkriterien 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:
Und wie wissen wir nun, wann wir diese User Story fertig gestellt haben? Es gibt verschiedene Scenarios zu berücksichtigen: Auf dem Konto ist Geld vorhanden; das Konto ist über die Überzugslimite überzogen; das Konto ist überzogen, aber noch innerhalb der Überzugslimite. Natürlich gibt es noch weitere Scenarios wie: Es hat noch Geld auf dem Konto, doch mit dem Bezug würde das Konto überzogen oder der Geldautomat hat zu wenig Geld.
Die beiden ersten Scenarios könnten mit der Angenommen-wenn-dann-Vorlage so aussehen:
Beachte die Verwendung von "und", um mehrere Bedingungen oder Ergebnisse auf natürliche Weise zu verknüpfen.
Beide Scenarios basieren auf dem gleichen Ereignis und haben sogar einige der Annahmen/Vorbedingungen und Ergebnisse gemein. Dies wollen wir hervorheben, indem wir die Vorbedingungen, Ereignisse und Ergebnisse möglichst wiederverwenden.
InhaltsverzeichnisDie Bestandteile eines Scenarios (Vorbedingungen, Ereignisse, Ergebnisse) sind genügend feingranular, um direkt in Programmcode umgesetzt zu werden. JBehave definiert ein Objektmodell, das es uns erlaubt, die Bestandteile des Scenarios direkt als Java-Klassen abzubilden.
Man schreibt eine Klasse für jede Vorbedingung:
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 Ergebnisse. Dann verknüpft JBehave alles und führt es aus. Es erzeugt eine "Welt" (World), die nur dazu dient, deine Objekte abzuspeichern, und übergibt sie an jede der Vorbedingungen, 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. Schlussendlich wird die Kontrolle an die Ergebnisse, die wir für die User Story definiert haben, übergeben.
Dass jede Klasse einem Bestandteil entspricht, erlaubt es uns, Bestandteile in anderen User Stories oder Scenarios wiederzuverwenden. Zu Beginn werden die Bestandteile mit Mocks umgesetzt, z.B. um ein Konto mit Geld zu haben oder eine gültige Bankkarte. Die Mocks bilden den Ausgangspunkt für die Implementation des Verhaltens. Wenn man die Applikation dann umsetzt, werden die Vorbedingungen und Ergebnisse nach und nach so abgeändert, dass sie die richtigen Klassen, die du inzwischen implementiert hast, verwenden. So entstehen bis zum Zeitpunkt, wenn das Scenario fertig ist, richtige und komplette funktionale Tests.
Inhaltsverzeichnis[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 Werbetrommel für BDD gerührt. Sein Blog und mehrere Artikel haben diverse Aktivitäten hervorgerufen. Am bemerkenswertesten davon ist das Projekt rspec, um ein BDD-Framework in der Sprache Ruby aufzubauen. Ich habe begonnen, an rbehave zu arbeiten. rbehave wird eine Implementation von JBehave in Ruby sein.
Einige meiner Arbeitskollegen haben BDD-Techniken auf einer Vielzahl von echten Projekten eingesetzt und schätzen die Methodik als sehr erfolgreich ein. Das JBehave-Ablauf-Tool — der Teil, der die Akzeptanzkriterien überprüft — wird aktiv weiterentwickelt.
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äftsumfeldes. Behaviour-Driven Development hat sich durch die Hilfe vieler Personen weiterentwickelt, und ich bin ihnen allen sehr dankbar!
InhaltsverzeichnisAbout Yulup
Made with in Zurich: