Um ein Projekt erfolgreich umzusetzen, ist ein kontinuierlicher Wissensaustausch essentieller Bestandteil für ein funktionierendes Team. Damit erreicht man, dass alle Teammitglieder nahezu den selben Wissenstand haben. Doch je länger ein Team zusammensitzt, desto geringer wird die Wissensvielfalt. Man spielt sich aufeinander ein und es fehlt an neuen Impulse.
Aktuell bin ich Teil eines vierköpfigen Teams und Codequalität, sowie Tests spielen bei uns eine übergeordnete Rolle. Ein gegenseitiges Code Review mit Hilfe des Code Review Tools Gerrit gehört genauso zu unserem Prozess wie Stand-Up Meetings und Testabdeckung. Doch wenn man in festen Teams arbeitet läuft man Gefahr Dinge mit „ach, es wissen doch alle im Team wie es geht“ abzutun.
Genau diesen Fall hatten wir bei unseren Tests. Die Funktionalität stand im Vordergrund. Über die Lesbarkeit hat sich keiner sonderlich viele Gedanken gemacht. Es wusste ja jeder wie es geht. Dass das Team getauscht werden oder andere Personen zusätzlich hinzukommen könnten, daran dachte beim Erstellen keiner, schließlich hat man ja auch etwas Zeitdruck.
So kam es, dass heute Morgen Jonas neben mir stand und fragte, ob ich etwas Zeit für Pairing hab. Wir schauten also erstmal über den existierenden Code. Da wir regelmäßig diverse Refactoring Runden im Team machen, gab es an der eigentlichen Applikation wenig zu bemängeln. Die Tests wieder rum wurden einfach so mitgeschleift. Hauptsache sie laufen und testen das, was sie testen sollen. Dies sollte natürlich nicht der Anspruch sein. Wir machten uns also dran den Test umzubauen. Diese Pairing Runden sind gut, da man den Code selbst reflektieren muss und auch gleich etwas lernt. So wie ich in diesem Fall über Behavior Driven Development.
Der Test
Beispielhaft zeige ich hier einen Test vor und nach dem Refactoring.
/* * CalculatorImpl.java */ @RunWith(JUnit4.class) public class CalculatorImpl { private Calculator calculator = new Calculator(); @Test public void firstNumberIsGreaterThanZero() { calculator.divide(20.0, 5.0); assertEquals(this.calculator.getResult(), Double.valueOf(4.0)); } @Test public void firstNumberIsEqualZero() { calculator.divide(20.0, 0.0); assertTrue(Double.isInfinite(this.calculator.getResult())); } }
Wir haben einen sehr simplen Calculator mit den Methoden divide(Double x, Double y) und getResult(). Testen wollen wir ob das Dividieren der ersten Zahl mit der zweiten Zahl zum korrekten Ergebnis führt. Wie wir alle aus der Schule wissen, kann man durch Null nicht dividieren. Daher bauen wir auch einen Test der diesen Fall prüft.
Unser Test nach dem Refactoring
Behavior Driven Development verfolgt das Ziel, dass die Funktionalität mit einfachen Sätzen beschrieben wird. In unserem Fall wären diesen Sätze wie folgt:
Gegeben ist eine Zahl 20 und eine weitere Zahl 5, wenn die erste Zahl mit der zweiten Zahl dividiert wird, dann sollte das Ergebnis 4 sein.
Gegeben ist eine Zahl 20 und eine weitere Zahl 0, wenn die erste Zahl mit der zweiten Zahl dividiert wird, dann sollte das Ergebnis Unendlich sein.
/* * CalculatorBDDImpl.java */ @RunWith(JUnit4.class) public class CalculatorBDDImpl { private Calculator calculator = new Calculator(); @Test public void firstNumberIsGreaterThanZero() { //given Double firstnumber = firstNumberIsTwenty(); //And Double secondNumber = SecondNumberIsFive(); //when divideTheFirstNumberByTheSecondNumber(firstnumber, secondNumber, this.calculator); //then theReturnValueShouldBeFour(); } @Test public void firstNumberIsEqualZero() { //given Double firstnumber = firstNumberIsTwenty(); //And Double secondNumber = secondNumberIsZero(); //when divideTheFirstNumberByTheSecondNumber(firstnumber, secondNumber, this.calculator); //then theReturnValueShouldBeInfinity(); } private Double SecondNumberIsFive() { return 5.0; } private Double firstNumberIsTwenty() { return 20.0; } private Double secondNumberIsZero() { return 0.0; } private void theReturnValueShouldBeFour() { assertEquals(calculator.getResult(), Double.valueOf(4.0)); } private void theReturnValueShouldBeInfinity() { assertTrue(Double.isInfinite(this.calculator.getResult())); } private void divideTheFirstNumberByTheSecondNumber(Double firstnumber, Double secondNumber, Calculator calculator) { this.calculator.divide(firstnumber, secondNumber); } }
Unsere Tests mit Toolunterstützung
Natürlich gibt es auch für Behavior Driven Development Toolunterstützung. Tools wie JBehave oder JDave können in den Entwicklungsprozess integriert werden. Dort schreibt man die Stories in Textform und arbeitet mit den Annotations @Given, @When, @Then.
Zu guter Letzt hier noch der endgültige Code mit JBehave Integration. Zu erst müssen Stories definiert werden.
Narrative: Math wizard divide 2 numbers As a math wizard I want to divide two numbers In order to get the result Scenario: First number is greater than second number Given the first number is twenty And the second number is five When the first number is divided by the second number Then the result should be returned four Scenario: Second number is zero Given the first number is twenty And the second number is zero When the first number is divided by the second number (zero) Then the result should be returned infinity
Eine kurze Erklärung zu den Schlüsselwörtern, die JBehave nutzt:
Narrative dient dazu die Userstory noch einmal zu beschreiben. Dieses Schlüsselwort wird aber von den Tests ignoriert und kann daher auch weggelassen werden.
Scenario(s) sind die einzelnen Testfälle. Diese werden mit einem kurzen Satz beschrieben.
Given, And, When und Then beschreiben den Testfall. Given beschreibt welche Voraussetzungen gegeben sein müssen. When sagt was ausgeführt werden muss. Dies muss zwingend ein Methodenaufruf sein. Then gibt an welches Ergebnis erwartet wird. Mit diesen Keywords erstellt JBehave aus den implementierten Schritten (siehe unten) einen Test.
/* * DivideSteps.java */ public class DivideSteps { private Double firstNumber; private Double secondNumber; private Calculator calculator = new Calculator(); @Given("the first number is twenty") public void createFirstNumber() { firstNumber = 20.0; } @Given("the second number is five") public void createSecondNumber() { secondNumber = 5.0; } @Given("the second number is zero") public void createSecondNumberAsZero() { secondNumber = 0.0; } @When("the first number is divided by the second number") public void divideFirstNumberGreaterThanZero() { calculator.divide(firstNumber, secondNumber); } @When("the first number is divided by the second number (zero)") public void divideFirstNumberEqualsZero() { calculator.divide(firstNumber, secondNumber); } @Then("the result should be returned four") public void resultShouldBeFour() { assertEquals(calculator.getResult(), Double.valueOf(4.0)); } @Then("the result should be return infinity") public void resultShouldBeInfinity() { assertTrue(Double.isInfinite(calculator.getResult())); } }
Die Demo-Applikation ist auf Github zu finden. Wer mehr über Behavior Driven Development erfahren will kann hier schauen. Informationen zu JBehave gibt es auf der offiziellen Seite.
Meine persönlichen Regeln für BDD:
1) Infinitest benutzen: http://infinitest.github.io/
Hat nix mit BDD zu tun, aber ohne permanenten roten oder grünen Balken macht mir Testen keinen Spaß: „if the bar is green, the code is clean“ 😉
2) keine Leerzeilen in Unit-Tests. Wenn ich eine Leerzeile brauche um die Übersichtlichkeit zu wahren, ist das ein Zeichen für zu hohe Komplexität, dann muss ich weiter am Abstraktionsgrad schrauben
3)
Test- Vorbedingungen (given) und Assertions (then) sind meist komplexer, weshalb ich sie zunächst in sprechende Hilfsmethoden auslagere, Bsp. „aTrialCustomer()“, „anExistingSessionCookie()“ etc.
Wird das Setup immer komplexer, weil das Domänen- oder Komponenten-Modell eben komplex ist, dann nutze ich das Builder-Pattern (http://en.wikipedia.org/wiki/Builder_pattern) zusammen mit Method-Chaining (http://en.wikipedia.org/wiki/Method_chaining) um mir dynamisch flexible Objektstrukturen zusammen zu bauen, das sieht dann so aus:
für’s Domain-Model:
// given
aTrialCustomer().named("Joe").registeredAt( new DateTime().minusDays(3) );
für Komponenten:
// given
aDummyMessageQueue().consumingEveryEventWithStatus("failed")
Das mache ich sowohl für die Erstellung von Value-Objekten, Komponenten und Entitäten, als auch für die Assertions. Gerade bei Entitäten bietet diese Abstraktion den deutlichen Vorteil, dass man nicht zig Unit-Tests ändern muss, wenn sich das Datenmodell verändert. Bei allen Vorgehensweisen, bei denen Testdaten separat vom Code abgespeichert werden (wie z.B. DB-Unit), ist das nachträgliche Anpassen oft katastrophal bis schlicht unmöglich, weil man später nicht mehr weiß, warum ein Test dieses und jedes Daten-Szenario brauchte.
4)
Den eigentlichen Test-Aufruf (//when) werde ich nie mittels Hilfsmethode abstrahieren, damit man den Einsprung in den Live-Code sofort sehen kann:
// given
user = ...
// when
cert = loginManager.authenticates(user);
// then
...
5)
Negativ-Tests nicht über @throws abbilden, sondern so:
// given
user = ...
// when
try {
loginManager.authenticates(user);
} catch (Exception ex) {
// then
// expected
}