Behavior Driven Development? – Wir wissen doch was wir tun!

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.

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …

Eine Antwort

  1. Jonas Kilian sagt:

    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
    }

Schreibe einen Kommentar

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