JGiven: Ein entwicklerfreundliches BDD-Framework für Java – Dr. Jan Schäfer – 27. November 2015



JGiven: Ein entwicklerfreundliches BDD-Framework für Java – Dr. Jan Schäfer – 27. November 2015

0 0


xpdays2015-slides

Slides for the presentation I give at XP Days Germany 2015

On Github janschaefer / xpdays2015-slides

JGiven: Ein entwicklerfreundliches BDD-Framework für Java

Dr. Jan Schäfer

27. November 2015

Müssen sich Entwickler beim Programmieren besser benehmen? Es wird etwas technischer.

Warum BDD?

Die meisten von Ihnen werden TDD einsetzen.

Typischer JUnit-Test

Kann nicht zur Kommunikation mit Domain-Experten genutzt werden Carola und Henning: Minimal viable product: Muss als Testfall formuliert werden können
@Test
public void shouldInsertPetIntoDatabaseAndGenerateId() {
    Owner owner6 = this.clinicService.findOwnerById(6);
    int found = owner6.getPets().size();

    Pet pet = new Pet();
    pet.setName("bowser");
    Collection<PetType> types = this.clinicService.findPetTypes();
    pet.setType(EntityUtils.getById(types, PetType.class, 2));
    pet.setBirthDate(new DateTime());
    owner6.addPet(pet);
    assertThat(owner6.getPets().size()).isEqualTo(found + 1);

    this.clinicService.savePet(pet);
    this.clinicService.saveOwner(owner6);

    owner6 = this.clinicService.findOwnerById(6);
    assertThat(owner6.getPets().size()).isEqualTo(found + 1);
    // checks that id has been generated
    assertThat(pet.getId()).isNotNull();
}
Beispiel von github.com/spring-projects/spring-petclinic

Probleme von JUnit-Tests

  • Viele technische Details
  • Eigentliche Kern des Tests oft schwer zu erkennen
  • Oft Code-Duplizierung
  • Können nur von Entwicklern gelesen werden

Verhaltensgetriebene Entwicklung (BDD)

  • Verhalten wird in der Fachsprache der Anwendung beschrieben
  • Entwickler und Fachexperten arbeiten gemeinsam an der Spezifikation
  • Verhaltensspezifikationen sind ausführbar
Working software over comprehensive documentation

TDD

Bauen wir die Anwendung richtig?

 

BDD

Bauen wir die richtige Anwendung?

BDD in Java

Feature-Dateien (Gherkin)

ordering.feature
Feature: Ordering

  Scenario: Customers can order books

    Given a customer
      And a book
      And 3 items of the book are on stock
     When the customer orders the book
     Then a corresponding order for the customer exists

Schritt-Implementierung (Java)

public class CustomerStepdefs {
    @Given("a customer")
    public void aCustomer() { ... }

    @Given("a book")
    public void aBook() { ... }

    @Given("(\\d+) items of the book are on stock")
    public void nItemsOfTheBookAreOnStock(int nItems) { ... }

    @When("the customer orders the book")
    public void theCustomerOrdersTheBook() { ... }

    @Then("a corresponding order for the customer exists")
    public void aCorrespondingOrderForTheCustomerExists() { ... }
}

Probleme

  • Feature-Dateien und Code müssen synchron gehalten werden
  • Keine Programmiersprache in Feature-Dateien verwendbar
  • Eingeschränkter IDE-Support (z.B. Refactoring)

Hohe Wartungskosten

Beobachtungen aus der Praxis

  • Niedrige Akzeptanz bei Entwicklern durch hohen Mehraufwand
  • Nicht-Entwickler schreiben selten Feature-Dateien selbst
  • Entwickler müssen Feature-Dateien warten
Carola und Henning. Werkzeuge können auch Folterwerkzeuge sein.

Andere BDD-Frameworks?

  • JBehave: Plain Text + Java (wie Cucumber)
  • Concordion: HTML + Java
  • Fitness: Wiki + Java
  • Spock: Groovy
  • ScalaTest: Scala
  • Jnario: Xtend
  • Serenity
 

Ziele

  • Entwicklerfreundlich (geringer Wartungsaufwand)
  • Lesbare Tests in Given-When-Then-Form (BDD)
  • Einfache Wiederverwendung von Test-Code
  • Lesbar für Fachexperten

Szenarien in JGiven

import org.junit.Test;
import com.tngtech.jgiven.junit.SimpleScenarioTest;

public class OrderTest extends SimpleScenarioTest<StepDefs> {

    @Test
    public void customers_can_order_books() {

        given().a_customer()
            .and().a_book_on_stock();

        when().the_customer_orders_the_book();

        then().a_corresponding_order_exists();
    }
}

Ok, aber was ist mit Nicht-Entwicklern?

Berichte

  • Text
  • HTML5 App
  • AsciiDoc (Alpha)

Textausgabe

Test Class: com.tngtech.jgiven.example.bookstore.OrderTest

 Scenario: customers can order books

   Given a customer
     And a book on stock
    When the customer orders the book
    Then a corresponding order exists for the customer

HTML5-App

Klassisches BDD

Lesbarer Text   Code

JGiven

Lesbarer Code   Lesbarer Bericht

Erfahrungen aus der Praxis

  • 2 Jahre Erfahrung aus einem großen Java-Projekt
  • Über 2000 Szenarien
  • Lesbarkeit und Wiederverwendung von Test-Code stark verbessert
  • Wartungskosten von automatisierten Tests veringert (keine harten Zahlen)
  • Entwickler und Fachexperten arbeiten gemeinsam an Szenarien
  • Große Akzeptanz bei Entwicklern
  • Leicht von neuen Entwicklern zu erlernen

Demo

Erste Schritte

JGiven-Abhängigkeit

  • com.tngtech.jgiven:jgiven-junit:0.9.5
  • oder com.tngtech.jgiven:jgiven-testng:0.9.5
  • Lizenz: Apache License v2.0

ScenarioTest* erweitern

import com.tngtech.jgiven.(junit|testng).ScenarioTest;

public class SomeScenarioTest extends ScenarioTest<...> {

}
*Oder SimpleScenarioTest

Stage-Typen hinzufügen

import com.tngtech.jgiven.junit.ScenarioTest;

public class SomeScenarioTest
    extends ScenarioTest<MyGivenStage, MyWhenStage, MyThenStage> {

}

Test-Methoden hinzufügen

import org.junit.Test;
import com.tngtech.jgiven.junit.ScenarioTest;

public class SomeScenarioTest
    extends ScenarioTest<MyGivenStage, MyWhenStage, MyThenStage> {

    @Test
    public void my_first_scenario() { ... }

}

Schritt-Methoden schreiben

import org.junit.Test;
import com.tngtech.jgiven.junit.ScenarioTest;

public class SomeScenarioTest
    extends ScenarioTest<MyGivenStage, MyWhenStage, MyThenStage> {

    @Test
    public void my_first_scenario() {

        given().some_initial_state();
        when().some_action();
        then().the_result_is_correct();

    }
}

Stage-Klassen schreiben

import com.tngtech.jgiven.Stage;

public class MyGivenStage extends Stage<MyGivenStage> {

    int state;

    public MyGivenStage some_initial_state() {
        state = 42;
        return this;
    }
}

Stage-Klassen

Allgemein

  • JGiven-Szenarien werden aus Stage-Klassen zusammengesetzt
  • Stage-Klassen ermöglichen Modularität und Wiederverwendung
  • Stage-Klassen sind ein Alleinstellungsmerkmal von JGiven, dass es in anderen BDD-Frameworks nicht gibt (Ausnahme: Serenity)

Zustandstransfer

Zustandstransfer

  • Felder von Stage-Klassen werden mit @ScenarioState annotiert
  • Werte werden zwischen Stages gelesen und geschrieben
  • @ProvidedScenarioState, @ExpectedScenarioState als Alternative
public class MyGivenStage extends Stage<MyGivenStage> {
   @ProvidedScenarioState
   int state;

   public MyGivenStage some_initial_state() {
      state = 42;
      return self();
   }
}

public class MyWhenStage extends Stage<MyWhenStage> {
   @ExpectedScenarioState
   int state;

   @ProvidedScenarioState
   int result;

   public MyWhenStage some_action() {
       result = state * state;
   }
}

Datengetriebene Szenarien

Parameterisierte Schrittmethoden

      given().a_customer_with_name( "John" );
          

Bericht

      Given a customer with name John
          

Mitten im Satz?

      Given there are 5 coffees left
          

$ to the rescue!

      given().there_are_$_coffees_left( 5 );
          

Parameterisierte Szenarien

@Test
@DataProvider({
   "1, 0",
   "3, 2",
   "5, 4"})
public void the_stock_is_reduced_when_a_book_is_ordered( int initial,
                                                         int left ) {
   given().a_customer()
       .and().a_book()
       .with().$_items_on_stock( initial );

   when().the_customer_orders_the_book();

   then().there_are_$_items_left_on_stock( left );
}
Verwendet den DataProviderRunner (github.com/TNG/junit-dataprovider). Parameterized Runner und Theories von JUnit sind auch unterstützt.

Parameterisierte Szenarien

Textausgabe

 Scenario: the stock is reduced when a book is ordered

   Given a customer
     And a book
    With <initial> items on stock
    When the customer orders the book
    Then there are <left> items left on stock

  Cases:

    | # | initial | left | Status  |
    +---+---------+------+---------+
    | 1 |       1 |    0 | Success |
    | 2 |       3 |    2 | Success |
    | 3 |       5 |    4 | Success |

Parameterisierte Szenarien

HTML-Bericht

Abgeleitete Parameter

@Test
@DataProvider({"1", "3", "5"})
public void the_stock_is_reduced_when_a_book_is_ordered( int initial ) {

   given().a_customer()
       .and().a_book()
       .with().$_items_on_stock( initial );

   when().the_customer_orders_the_book();

   then().there_are_$_items_left_on_stock( initial - 1 );

}

Abgeleitete Parameter

Textausgabe

 Scenario: the stock is reduced when a book is ordered

   Given a customer
     And a book
    With <initial> items on stock
    When the customer orders the book
    Then there are <numberOfItems> items left on stock

  Cases:

    | # | initial | numberOfItems | Status  |
    +---+---------+---------------+---------+
    | 1 |       1 |             0 | Success |
    | 2 |       3 |             2 | Success |
    | 3 |       5 |             4 | Success |

Abgeleitete Parameter

HTML-Bericht

Verschiedene Schritte

@Test
@DataProvider({ "3, 2, true",
                "0, 0, false" })
public void the_stock_is_only_reduced_when_possible(
         int initial, int left, boolean orderExists) {

    given().a_customer()
        .and().a_book()
        .with().$_items_on_stock( initial );

    when().the_customer_orders_the_book();

    if ( orderExists ) {
        then().a_corresponding_order_exists_for_the_customer();
    } else {
        then().no_corresponding_order_exists_for_the_customer();
    }
}

Verschiedene Schritte

Textausgabe

Scenario: the stock is only reduced when possible

  Case 1: initial = 3, left = 2, orderExists = true
   Given a customer
     And a book
    With 3 items on stock
    When the customer orders the book
    Then there are 2 items left on stock
     And a corresponding order exists for the customer

  Case 2: initial = 0, left = 0, orderExists = false
   Given a customer
     And a book
    With 0 items on stock
    When the customer orders the book
    Then there are 0 items left on stock
     And no corresponding order exists for the customer

Verschiedene Schritte

HTML-Bericht

Weitere Features

Parameter-Formatierung

  • Default: toString()
  • @Format( MyCustomFormatter.class )
  • @Formatf( " -- %s -- " )
  • @MyCustomFormatAnnotation

Beispiel

@OnOff

@Format( value = BooleanFormatter.class, args = { "on", "off" } )
@Retention( RetentionPolicy.RUNTIME )
@interface OnOff {}

Auf Parameter anwenden

public SELF the_machine_is_$( @OnOff boolean onOrOff ) { ... }

Schritt benutzen

given().the_machine_is_$( false );

Bericht

Given the machine is off

Tabellen als Parameter

  • @Table um einen Parameter als Tabelle zu markieren
  • Muss der letzte Parameter sein
  • Muss ein Iterable of Iterable, ein Iterable of POJOs, oder ein POJO sein

Tabellen als Parameter

Arrays

SELF the_following_books_are_on_stock( @Table String[][] stockTable ) {
   ...
}
  • Die erste Zeile ist der Tabellenkopf

Tabellen als Parameter

Arrays

@Test
public void ordering_a_book_reduces_the_stock() {

    given().the_following_books_on_stock(new String[][]{
        {"id", "name", "author", "stock"},
        {"1", "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", "5"},
        {"2", "Lord of the Rings", "John Tolkien", "3"},
    });

    when().a_customer_orders_book("1");

    then().the_stock_looks_as_follows(new String[][]{
        {"id", "name", "author", "stock"},
        {"1", "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", "4"},
        {"2", "Lord of the Rings", "John Tolkien", "3"},
    });

}

Tabellen als Parameter

Textausgabe

Scenario: ordering a book reduces the stock

   Given the following books on stock

     | id | name                                 | author        | stock |
     +----+--------------------------------------+---------------+-------+
     |  1 | The Hitchhiker's Guide to the Galaxy | Douglas Adams |     5 |
     |  2 | Lord of the Rings                    | John Tolkien  |     3 |

    When a customer orders book 1
    Then the stock looks as follows

     | id | name                                 | author        | stock |
     +----+--------------------------------------+---------------+-------+
     |  1 | The Hitchhiker's Guide to the Galaxy | Douglas Adams |     4 |
     |  2 | Lord of the Rings                    | John Tolkien  |     3 |

Tabellen als Parameter

HTML-Bericht

Tabellen als Parameter

Liste von POJOs

  • Feldnamen: Kopf
  • Feldwerte: Daten
SELF the_following_books_are_on_stock( @Table List<BookOnStock> books) {
   ...
}

Tabellen als Parameter

Einfaches POJO

SELF the_following_book(
      @Table(includeFields = {"name", "author", "priceInEurCents"},
             header = VERTICAL) Book book) {
   ...
}

HTML-Bericht

@BeforeScenario und @AfterScenario

public class GivenSteps extends Stage<GivenSteps> {

    @ProvidedScenarioState
    File temporaryFolder;

    @BeforeScenario
    void setupTemporaryFolder() {
	    temporaryFolder = ...
    }

    @AfterScenario
    void deleteTemporaryFolder() {
        temporaryFolder.delete();
    }
}

@ScenarioRule

public class TemporaryFolderRule {
    File temporaryFolder;

    public void before() {
        temporaryFolder = ...
    }

    public void after() {
        temporaryFolder.delete();
    }
}

public class GivenSteps extends Stage<GivenSteps> {
    @ScenarioRule
    TemporaryFolderRule rule = new TemporaryFolderRule();
}

@AfterStage, @BeforeStage

public class GivenCustomer extends Stage<GivenSteps> {
    CustomerBuilder builder;

    @ProvidedScenarioState
    Customer customer;

    public void a_customer() {
        builder = new CustomerBuilder();
    }

    public void with_age(int age) {
        builder.withAge(age);
    }

    @AfterStage
    void buildCustomer() {
        customer = builder.build();
    }
}

Tags

@Test @FeatureEmail
void the_customer_gets_an_email_when_ordering_a_book() {
   ...
}

Mit Werten

@Test @Story( "ABC-123" )
void the_customer_gets_an_email_when_ordering_a_book() { ... }

@Pending

  • Markiert den ganzen Test oder einzelne Schrittmethoden als noch nicht implementiert
  • Schritte werden übersprungen und im Bericht entsprechend markiert

HTML-Bericht

@Hidden

  • Markiert Methoden die nicht im Bericht erscheinen sollen
  • Sinnvoll für Methoden, die technisch gebraucht werden
@Hidden
public SELF doSomethingTechnical() { ... }

Erweiterte Schrittbeschreibungen

@ExtendedDescription("The Hitchhiker's Guide to the Galaxy, "
                   + "by default the book is not on stock" )
public SELF a_book() { ... }

HTML-Bericht

Anhänge

public class Html5ReportStage {
    @ExpectedScenarioState
    protected CurrentStep currentStep; // provided by JGiven

    protected void takeScreenshot() {
        String base64 = ( (TakesScreenshot) webDriver )
            .getScreenshotAs( OutputType.BASE64 );
        currentStep.addAttachment(
            Attachment.fromBase64( base64, MediaType.PNG )
                      .withTitle( "Screenshot" ) );
    }
}

HTML-Bericht

Zusammenfassung

Vorteile

  • Entwicklerfreundlich
  • Hohe Modularität und Wiederverwendung von Test-Code
  • Reines Java, keine weitere Sprache nötig
  • Sehr leicht zu erlernen
  • Sehr leicht in bestehende Test-Infrastrukturen zu integrieren
  • Lesbare Berichte für Fachexperten

BDD ohne den Zusatzaufwand!

Nachteile

  • Fachexperten können keine JGiven-Szenarien schreiben
    • Aber Akzeptanzkriterien können leicht in JGiven-Szenarien übersetzt werden
    • Die generierten Berichte können von Fachexperten gelesen werden

Danke!

@JanSchfr

jgiven.org

github.com/TNG/JGiven

janschaefer.github.io/xpdays2015-slides

Backup

Warum snake_case?

  • Besser lesbar
    • thisCannotBeReadVeryEasilyBecauseItIsCamelCase
    • this_can_be_read_much_better_because_it_is_snake_case
  • Wörter können in korrekter Schreibweise geschrieben werden
    • given().an_HTML_page()
  • Berichtgenerierung funktioniert nur sinnvoll mit snake_case
JGiven: Ein entwicklerfreundliches BDD-Framework für Java Dr. Jan Schäfer 27. November 2015 Müssen sich Entwickler beim Programmieren besser benehmen? Es wird etwas technischer.