Erzeugung von Testobjekten mit dem Builder-Pattern (lesbar und flexibel)

02.10.2020

Tests prüfen und dokumentieren Quellcode. Daher sollten sie sauber und verständlich sein. Die Erzeugung von Testobjekten stört oft den Lesefluss. Insbesondere dann, wenn mehrere Varianten benötigt werden, die sich nur in wenigen Werten unterscheiden. Hier hilft das Builder-Pattern enorm.

Ausgangssituation ohne Builder

Folgendes Listing zeigt einen kleinen Ausschnitt der Instanzierung von Test-Objekten. Man kann sich leicht vorstellen, wie dies mit mehreren Address-Objekten aussieht…

...
Address address = new Address("1");
address.setSalutation("Herr");
address.setTitle("Dr.");
address.setFirstname("Max");
address.setLastname("Mustermann");
address.setAddressType("MainAddress");
address.setStreet("Musterstraße");
address.setHouseNumber("123");
address.setZipCode("12345");
address.setCity("Musterstadt");
address.setEmail("max@mustermann.de");
address.setVerified(true);
address.setHistoric(false);
...
  • Geschwätzig und unübersichtlich. Der Leser tut sich schwer, Initialisierung von Testobjekten von der tatsächlichen Testlogik abzugrenzen. Dies gilt vor allem dann, wenn man mehrere solcher Objekte erzeugen muss, die sich nur in wenigen Attributwerten unterscheiden.
  • Aufwändig, wenn man solche Instanzierungen häufig vornehmen muss. => Man hilft sich manchmal mit Erzeuger-Methoden.
  • Bei diesen besteht jedoch die Gefahr, dass die Parameter-Reihenfolge versehentlich vertauscht wird (auch wenn moderne IDEs hier durch Hints helfen). Richtig hässlich wird es aber, wenn später weitere, leicht veränderte Varianten benötigt werden… 🙁
  • Bei Erzeugung mehrere Testobjekte besteht außerdem die Gefahr, dass man beim Kopieren vergisst, die Referenz anzupassen:
    ...
    Address address1 = new Address("1");
    address1.setSalutation("Herr");
    address1.setTitle("Dr.");
    ...
    Address address2 = new Address("2"); 
    address1.setSalutation("Frau"); 
    address1.setTitle("Dr."); 
    ...
    

 

Mit einfachem Builder

AddressBuilder UML
generiert via www.plantuml.com
...
Address address = new AddressBuilder("1")
        .withSalutation("Herr")
        .withTitle("Dr.")
        .withFirstname("Max")
        .withLastname("Mustermann")
        .withAddressType("MainAddress")
        .withStreet("Musterstraße")
        .withHouseNumber("123")
        .withZipCode("12345")
        .withCity("Musterstadt")
        .withEmail("max@mustermann.de")
        .verified()
        .notHistoric()
        .build();
...
  • Fluent-API fördert die Lesbarkeit und trennt durch Einrückung Objektinitialisierung besser vom restlichen Testcode.
  • Vermeidet die ständige Wiederholung der Variablen address.
  • Verhindert versehentliches Vertauschen der Parameter-Reihenfolge. Zwar helfen hier manche IDEs durch Einblenden der Parameternamen (z.B. IntelliJ), jedoch ist dies dem Lesefluss auch nicht so richtig zuträglich.
  • der Builder kann einfach in anderen Tests wiederverwendet werden. Daher lohnt sich meist der Mehraufwand bei der Erstellung (IDE-Plugins, sowie die mächtigen Such-/Ersetzen-Funktionen moderner IDEs helfen hierbei).

Hinweis: Wir setzen in unserem Fall ein etwas vereinfachtes Builder-Pattern (single field) ein. Außerdem ist unser Builder keine innere Klasse der zu erzeugenden Klasse sondern eigenständig. Dadurch muss die ursprüngliche Klasse aus dem Produktivcode nicht verändert werden. Dies ist wichtig, da der Builder spezielle Hilfsmethoden enthalten soll, die nur für Tests relevant sind und uns das Leben hier vereinfachen. Diese gehören natürlich nicht in den Produktivcode. Häufige Initialisierungen lassen sich auf diese Weise verbergen – wie wir gleich sehen werden.

Mit speziellem Testdaten-Builder

...
Address address = new AddressTestdataBuilder("1")
    .musterAddress() // spezielle Hilfsmethode für Tests
    .withEmail("max@mustermann.de")
    .verified()
    .notHistoric()
    .build();
...

Zusätzlich zu den Vorteilen des einfachen Builders kommt hier Folgendes hinzu:

  • Information Hiding und Fokussierung auf die für den Test relevanten Attribute:
    Wiederkehrendes kann versteckt werden, ohne Lesbarkeit einzubüßen: Die Max-Mustermann-Adresse kommt immer wieder in Tests vor. Dabei kommt es in den Tests hauptsächlich auf die weiteren Attribute an. Mit einem speziellen Testdatenbuilder lassen sich wiederkehrende Initialisierungen, die nur dazu dienen, dass der Code keine Fehler wirft, in Hilfsmethoden verlagern.
    => So kann man Unwichtiges verstecken und die für den Test wichtigen Attribute hervorheben (in diesem Fall sind dies die Attribute email, verified und historic). 🙂
  • Erzeugung unterschiedlicher Repräsentationen möglich:
    Fügt man dem Builder weitere build-Methoden hinzu, lassen sich dadurch ohne großen Aufwand verschiedene Repräsentationen erzeugen. Wir haben dies zum Beispiel in Integrationstests genutzt: Eine build-Methode erzeugt JSON für die Anlage des Testdatensatzes in der NoSQL-Datenbank. Eine weitere generiert das Löschstatement, um die Testdaten wieder aufzuräumen.

Der Testobjekt-Builder sieht in unserem Beispiel wie folgt aus:

public class AddressBuilder {  
  
    private Address address;  
     
    public static AddressBuilder builder() {  
        return new AddressBuilder();  
    }  
  
    public AddressBuilder() {  
         this.address = new Address();  
    }  
  
    public AddressBuilder(final String id) {  
        this.address = new Address(id);  
    }  
  
    public AddressBuilder withId(final String id) {  
        this.address.setId(id);  
        return this;  
    }  
  
    public AddressBuilder withSalutation(final String salutation) {  
        this.address.setSalutation(salutation);  
        return this;  
    }  
  
    public AddressBuilder withTitle(final String title) {  
        this.address.setTitle(title);  
        return this;  
    }  
  
    public AddressBuilder musterAddress() {  
  
	    maxMustermann();  
          
        this.address.setStreet("Musterstraße");  
        this.address.setHouseNumber("123");  
        this.address.setZipCode("12345");  
        this.address.setCity("Musterstadt");  
        this.address.setAddressType("MainAddress");  
  
        return this;  
    }  
  
    public AddressBuilder maxMustermann() {  
        this.address.setSalutation("Herr");  
        this.address.setTitle("Dr.");  
        this.address.setFirstname("Max");  
        this.address.setLastname("Mustermann");  
        return this;  
    }  
  
    public AddressBuilder withName(final String firstname, final String lastname) {  
        this.address.setFirstname(firstname);  
        this.address.setLastname(lastname);  
        return this;  
    }  
  
    public AddressBuilder withEmail(final String email) {  
        this.address.setEmail(email);  
        return this;  
    }  
  
    public AddressBuilder verified() {  
        return withVerified(true);  
    }  
  
    public AddressBuilder notVerified() {  
        return withVerified(false);  
    }  
  
    public AddressBuilder withVerified(final boolean verified) {  
        this.address.seVerified(verified);  
        return this;  
    }  
  
    public AddressBuilder historic() {  
        return withHistoric(true);  
    }  
  
    public AddressBuilder notHistoric() {  
        return withHistoric(false);  
    }  
  
    public AddressBuilder withHistoric(final boolean historic) {  
        this.address.setHistoric(historic);  
        return this;  
    }  
  
    public Address build() {  
        return this.address;  
    }  
  
    // ggf. weitere build-Methoden für spezielle Repräsentationen  
  
}

 

Fazit

Das Builder-Pattern hilft bei der Instanzierung von Testobjekten. Es erlaubt eine Fokussierung auf die relevanten Attribute. Durch die automatische Einrückung durch die IDE kann man Testobjekt-Instanzierung und Testlogik leichter unterscheiden. Beides führt zu besserer Lesbarkeit der Tests. Der Builder lässt sich außerdem sehr leicht für weitere Tests wiederverwenden – auch dann, wenn Varianten oder komplett unterschiedliche Repräsentationen benötigt werden. Der Mehrwert ist enorm und wiegt den Mehraufwand beim Erstellen des Builders meist auf. 😎

 

👉 Meine Empfehlung ist daher, das Builder-Pattern gleich von Anfang an einzusetzen. Ein nachträglicher Umbau ist aufwändig und macht wenig Spass.

 

Zu guter Letzt noch ein Vorher-/Nachher-Beispiel aus einem realen Projekt:

 

Mehr zur Verwendung des Builder-Patterns bei Unit-Tests siehe Folgeartikel Erzeugung von Mocks mit dem Builder-Pattern.

 

Links:

 

Zurück zur Übersicht

Ein Kommentar zur “Erzeugung von Testobjekten mit dem Builder-Pattern (lesbar und flexibel)

Kommentar verfassen

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

*Pflichtfelder

*