Lessons learned

Behavior-driven Development: Akzeptanztests in Java mit JGiven

Marco Schulz

© maljuk/Shutterstock.com

Die meisten Entwickler wissen, was Komponententests sind, auch wenn sie selbst keine schreiben. Aber es gibt noch Hoffnung: Die Situation ändert sich, und immer mehr auf GitHub gehostete Projekte enthalten Unit-Tests. In einer Standardkonfiguration für Java-Projekte mit NetBeans, Maven und JUnit ist es nicht ganz so schwer, die ersten Zeilen Testcode zu erstellen. Neben dem Ansatz, der in Test-driven Development (TDD) verwendet wird, gibt es noch andere Technologien wie Behavior-driven Development (BDD), auch Akzeptanztests genannt, die wir in diesem Artikel vorstellen möchten.

Der leichteste Weg, um in das Thema einzusteigen, ist ein einfacher Vergleich zwischen Unit- und Akzeptanztests. In diesem Zusammenhang sind Unit-Tests auf sehr niedrigem Niveau angesiedelt. Es wird eine Funktion ausgeführt und die Ausgabe mit einem zu erwarteten Ergebnis verglichen. Allerdings teilen nicht alle die Meinung, dass der Entwickler als einziger für die Unit-Tests verantwortlich ist. Schließlich liegt der Testcode auch im Projektverzeichnis und wird mit jedem Build ausgeführt. Das wiederrum führt dazu, dass man sehr früh Rückmeldung erhält, ob etwas schiefgelaufen ist. Solange der Test nicht zu viele Aspekte abdeckt, kann das Problem recht schnell identifiziert und behoben werden. Das Designprinzip solcher Tests folgt dem AAA-Paradigma.

Zuerst wird eine Vorbedingung definiert (Arrange), dann die Invariante exekutiert (Act), um anschließend die Nachbedingungen (Assume) zu überprüfen. Auf diesen Ansatz werden wir später noch einmal zurückkommen. Wenn wir die Testabdeckung mit Werkzeugen wie JaCoCo überprüfen und unser Code zu mehr als 85 Prozent mit Testfällen abgedeckt ist, können wir von einer guten Qualität ausgehen. Während der Erhöhung der Testabdeckung werden die Testfälle immer konkreter spezifiziert und dadurch auch genauer und das versetzt uns in die Lage, eigene Optimierungsmöglichkeiten zu identifizieren. Das kann das Entfernen oder Invertieren von Bedingungen sein, da während der Tests festgestellt wird, dass es beinah unmöglich ist, diese Abschnitte zu erreichen. Natürlich ist das Thema etwas komplizierter und umfangreicher, weswegen diese Details in einem anderen Artikel besprochen werden sollten.

Abnahmetests sind genauso zu klassifizieren wie Komponententests – sie gehören zur Familie der Regressionstests. Das heißt, im Grunde möchten wir sicherstellen, dass Änderungen, die wir am Code vorgenommen haben, keine Auswirkungen auf die bereits fertiggestellte Funktionalität haben. Das Werkzeug unserer Wahl hierfür ist JGiven. Bevor wir nun mit einigen Beispielen fortfahren, müssen wir aber noch ein bisschen Theorie besprechen.

JGiven im Detail

Die Testfälle, die wir in JGiven definieren, heißen Szenarien. Ein Szenario ist eine Sammlung von vier Klassen, nämlich: das Szenario selbst, das als given angezeigte Arrange, die als when angezeigte Action (Act) und das als then angezeigte Outcome (Assume). In den meisten Projekten, insbesondere wenn eine Vielzahl von Szenarien vorliegt und die Ausführung viel Zeit in Anspruch nimmt, werden Abnahmetests in einem separaten Projekt organisiert. Mit einem Build-Job auf dem CI-Server können Sie diese Tests einmal täglich ausführen, um schnelles Feedback zu erhalten und um frühzeitig zu reagieren, falls etwas kaputt geht. Das hier gezeigte Codebeispiel ist vollständig als Projekt auf GitHub verfügbar. Da es sich nur um eine kleine Bibliothek handelt, wäre eine Aufteilung in zwei Artefakte etwas übertrieben. Bei Akzeptanztests ist üblicherweise das Testzentrum für die Abnahme verantwortlich und nicht der Entwickler.

TP-CORE ist in einer Layerarchitektur organisiert. Für die folgende Demonstration wählen wir die Funktionalität zum Versenden von E-Mails aus. Die Grundfunktionalität zum Verfassen einer E-Mail wird im Application Layer realisiert und hat eine Testabdeckung von über 90 Prozent. Die Funktionalität zum Versenden der Mail wird im Service Layer definiert. In unserer Architektur haben wir uns dazu entschlossen, die Service-Ebene als Mittelpunkt für die Definition von Abnahmetests zu verwenden. Hier wollen wir sehen, ob unsere Anforderung, eine E-Mail zu senden, gut funktioniert. Die Unterstützung dieser Ebene mit eigenen Unit-Tests ist in diesem Fall nicht effizient, da sie in kommerziellen Projekten nur Kosten verursacht und keine Vorteile bringt. Zudem bedeuten Unit-Tests, die Arbeit doppelt zu machen, denn unsere JGiven-Tests zeigen bereits, ob unsere Funktion gut funktioniert. Aus diesem Grund ist es wenig hilfreich, eine Testabdeckung für die Testszenarien des Abnahmetests zu generieren.

Beginnen wir mit der Praxis. Als erstes müssen wir unser Akzeptanztest-Framework in unseren Maven Build einbinden. Wer Gradle bevorzugt, kann dieselben GAV-Parameter verwenden, um die Abhängigkeiten im Build-Skript zu definieren.

<dependency>
  <groupId>com.tngtech.jgiven</groupId>
  <artifactId>jgiven-junit</artifactId>
  <version>0.18.2</version>
  <scope>test</scope>
</dependency>

Wie in Listing 1 zu sehen ist, arbeitet JGiven gut mit JUnit zusammen. Bei der Integration mit TestNG müssen Sie nur artifactId für jgiven-testng austauschen. Informationen zum Aktivieren der HTML-Berichte, die Sie mit dem Maven-Plug-in im Build-Lebenszyklus erstellen, finden Sie in Listing 2.

<build>
  <plugins>
    <plugin>
      <groupId>com.tngtech.jgiven</groupId>
      <artifactId>jgiven-maven-plugin</artifactId>
      <version>0.18.2</version>
      <executions>
        <execution>
          <goals>
            <goal>report</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <format>html</format>
      </configuration>
    </plugin>
  </plugins>
</build>

Der Bericht zu den definierten Szenarien in TP-CORE ist in Abbildung 1 dargestellt. Wie wir sehen können, ist die Ausgabe sehr sprechend und für Menschen lesbar. Dieses Ergebnis wird erreicht, indem einige Namenskonventionen für unsere Methoden und Klassen eingehalten werden, die im Folgenden ausführlich erläutert werden. Besprechen wir zunächst, was wir in unserem Testszenario sehen können. Wir haben vier Vorbedingungen definiert:

  • Die Konfiguration für den SMTP-Server ist lesbar.
  • Der SMTP-Server ist verfügbar.
  • Die Mail hat Anhänge.
  • Die Mail ist vollständig verfasst.

Wenn all diese Bedingungen erfüllt sind, wird durch die Aktion eine einzelne E-Mail gesendet, die ausgeführt wurde. Anschließend wird auf dem SMTP-Server geprüft, ob die Mail angekommen ist.

Um den SMTP-Dienst testen zu können, nutzen wir die kleine Bibliothek GreenMail, die lokal einen einfachen SMTP-Server emuliert. Nun lässt sich auch nachvollziehen, weswegen es bei Akzeptanztests vorteilhaft ist, wenn sie von anderen Personen geschrieben werden: Es erhöht die Qualität, da so bereits frühzeitig konzeptionelle Unstimmigkeiten zum Vorschein kommen. Denn solange der Tester mit den verfügbaren Implementierungen das geforderte Szenario nicht abbilden kann, ist die Anforderung nicht vollständig umgesetzt.

Abb 1: Szenarien in TP-CORE

Abb 1: Szenarien in TP-CORE

Verständliche Testszenarien erstellen

Das ist ein guter Zeitpunkt, um sich eingehend mit den Implementierungsdetails unseres E-Mail-Testszenarios zu befassen. Unser Testobjekt ist die Klasse MailClientService. Die entsprechende Testklasse ist MailClientScenarioTest und wurde in den Testpaketen definiert. Die Klassendefinition des Szenarios ist in Listing 3 dargestellt.

@RunWith(JUnitPlatform.class)
public class MailClientScenarioTest extends ScenarioTest<MailServiceGiven, MailServiceAction, MailServiceOutcome> {
  // do something
}

Wie wir sehen können, führen wir das Test-Framework mit JUnit 5 aus. Im ScenarioTest werden die drei Klassen Given, Action und Outcome in einer speziellen Namenskonvention angelegt. Es ist auch möglich, bereits definierte Klassen wiederzuverwenden, allerdings sollte mit solchen Praktiken sehr vorsichtig umgegangen werden – das kann einige unerwünschte Nebenwirkungen hervorrufen. Bevor wir nun die Testmethode implementieren, müssen wir die Ausführungsschritte definieren. Die Vorgehensweise für die drei Klassen ist äquivalent.

@RunWith(JUnitPlatform.class)
public class MailServiceGiven extends Stage<MailServiceGiven> {
 
  public MailServiceGiven email_has_recipient(MailClient client) {
    try {
      assertEquals(1, client.getRecipentList().size());
    } catch (Exception ex) {
      System.err.println(ex.getMessage);
    }
    return self();
  }
}
 
@RunWith(JUnitPlatform.class)
public class MailServiceAction extends Stage<MailServiceAction> {
 
  public MailServiceAction send_email(MailClient client) {
    MailClientService service = new MailClientService();
    try {
      assertEquals(1, client.getRecipentList().size());
      service.sendEmail(client);
    } catch (Exception ex) {
      System.err.println(ex.getMessage);
    }
    return self();
  }
}
 
@RunWith(JUnitPlatform.class)
public class MailServiceOutcome extends Stage<MailServiceOutcome> {
 
  public MailServiceOutcome email_is_arrived(MimeMessage msg) {
  try {
    Address adr = msg.getAllRecipients()[0];
    assertEquals("JGiven Test E-Mail", msg.getSubject());
    assertEquals("noreply@sample.org", msg.getSender().toString());
    assertEquals("otto@sample.org", adr.toString());
    assertNotNull(msg.getSize());
  } catch (Exception ex) {
    System.err.println(ex.getMessage);
  }
  return self();
}

Manch einer wird sich nun fragen, wo die assert-Methoden herkommen. Die Antwort ist sehr einfach: Das ist die Integration des JUnit-5-Test-Frameworks, das wir verwenden. Das heißt, wenn eine Annahme fehlschlägt, schlägt auch das gesamte Szenario fehl. Wir können außerdem sehen, wie die Namen der Methoden mit der Ausgabe des Berichts korrespondieren. Das zeigt uns, wie wichtig gut gewählte Namen sind, um den Kontext verständlich zu halten. Nachdem wir die Schritte für das Bestehen des Szenarios definiert haben, müssen wir sie in der Szenariotestklasse kombinieren. Wie wir das umsetzten, zeigt Listing 5; wir erweitern die Implementierung aus Listing 3 um die Funktion scenario_sendSingleEmail ().

private MailClient client = null;
 
public MailClientScenarioTest() {
  client = new MailClientImpl();
  //COMPOSE MAIL
  client.loadConfigurationFromProperties("org/europa/together/properties/mail-test.properties");
  client.setSubject("JGiven Test E-Mail");
  client.setContent(StringUtils.generateLoremIpsum(0));
  client.addAttachment("Attachment.pdf");
}
 
@Test
void scenario_sendSingleEmail() {
 
  client.clearRecipents();
  client.addRecipent("otto@sample.org");
  try {
    // PreCondition
    given().email_get_configuration(client)
      .and().smpt_server_is_available()
      .and().email_has_recipient(client)
      .and().email_contains_attachment(client)
      .and().email_is_composed(client);
 
    // Invariant
    when().send_email(client);
 
    //PostCondition
    then().email_is_arrived(SMTP_SERVER.getReceivedMessages()[0]);
 
  } catch (Exception ex) {
    System.err.println(ex.getMessage);
  }
}

Jetzt haben wir den Zyklus abgeschlossen und können sehen, wie die Testschritte zusammengehalten wurden. JGiven unterstützt ein größeres Vokabular, um mehr Notwendigkeiten zu erfüllen. Um alle Möglichkeiten zu erkunden, konsultieren Sie bitte die Dokumentation.

Fazit

In diesem kurzen Workshop haben wir alle wichtigen Details angesprochen, um mit automatisierten Abnahmetests zu beginnen. Neben JGiven gibt es noch andere Frameworks wie Concordion oder FitNesse die um die Gunst der Nutzer buhlen. Ausschlaggebend für die Wahl von JGiven war die ausführliche Dokumentation, die einfache Integration in Maven Builds und JUnit-Tests sowie die für Menschen gut lesbaren Reports. Zu den negativen Punkten, die die Leute von JGiven fernhalten könnten, zählt der Umstand, dass Sie die Tests in Java schreiben müssen. Das bedeutet, dass der Testingenieur in der Lage sein muss, in Java zu entwickeln. Abgesehen von diesem kleinen Detail sind unsere Erfahrungen mit JGiven absolut positiv.

Verwandte Themen:

Geschrieben von
Marco Schulz
Marco Schulz
Marco Schulz studierte an der HS Merseburg Diplominformatik. Sein persönlicher Schwerpunkt liegt in Software Architekturen, der Automatisierung des Softwareentwicklungs-Prozess und dem Softwarekonfigurationsmanagement. Seit über fünfzehn Jahren realisiert er in internationalen Projekten für nahmenhafte Unternehmen auf unterschiedlichen Plattformen umfangreiche Webapplikationen. Er ist freier Consultant, Trainer und Autor verschiedener Fachartikel. Sein persönlicher Blog lautet https://enRebaja.wordpress.com, sie erreichen ihn unter: marco.schulz@outlook.com
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: