Erzeugung von Testobjekten mit dem Builder-Pattern (lesbar und flexibel)
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
Aus Platzgründen wollen wir hier nur einen Ausschnitt der Testobjekt-Instanzierungen abbilden. Man kann sich jedoch leicht vorstellen wie dies mit mehreren Testobjekten aussieht…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... 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:
123456789...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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... 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: Der Builder ist hier als eigene Klasse umgesetzt. Das entspricht nicht 100% dem echten Builder-Pattern. Es hat jedoch folgende Vorteile: Man muss die ursprüngliche Klasse aus dem Produktivcode nicht verändern (dies sollte man auch nicht tun, wenn es sich um Code handelt, der nur für Tests relevant ist). Dadurch kann der Builder spezielle Methoden erhalten, die nur für Tests bestimmt sind. Häufige Initialisierungen lassen sich so verbergen – wie wir gleich noch sehen werden.
Mit speziellem Testdaten-Builder
1 2 3 4 5 6 7 8 |
... Address address = new AddressTestdataBuilder("1") .musterAddress() .withEmail("max@mustermann.de") .verified() .notHistoric() .build(); ... |
- 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 Initialisierung, 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
public class AddressBuilder { private String id; private String salutation; private String title; private String firstname; private String lastname; private String addressType; private String street; private String houseNumber; private String zipCode; private String city; private String email; private boolean verified; private boolean historic; public static AddressBuilder builder() { return new AddressBuilder(); } public AddressBuilder() { } public AddressBuilder(final String id) { this.id = id; } public AddressBuilder withId(final String id) { this.id = id; return this; } public AddressBuilder withSalutation(final String salutation) { this.salutation = salutation; return this; } public AddressBuilder withTitle(final String title) { this.title = title; return this; } public AddressBuilder musterAddress() { maxMustermann(); this.street = "Musterstraße"; this.houseNumber = "123"; this.zipCode = "12345"; this.city = "Musterstadt"; this.addressType = "MainAddress"; return this; } public AddressBuilder maxMustermann() { this.salutation = "Herr"; this.title = "Dr."; this.firstname = "Max"; this.lastname = "Mustermann"; return this; } public AddressBuilder withName(final String firstname, final String lastname) { this.firstname = firstname; this.lastname = lastname; return this; } public AddressBuilder withEmail(final String email) { this.email = email; return this; } public AddressBuilder verified() { return withVerified(true); } public AddressBuilder notVerified() { return withVerified(false); } public AddressBuilder withVerified(final boolean verified) { this.verified = verified; return this; } public AddressBuilder historic() { return withHistoric(true); } public AddressBuilder notHistoric() { return withHistoric(false); } public AddressBuilder withHistoric(final boolean historic) { this.historic = historic; return this; } public Address build() { final Address address = new EsData(); address.setId(this.id); address.setSalutation(this.salutation); address.setTitle(this.title); address.setFirstname(this.firstname); address.setLastname(this.lastname); address.setAddressType(this.addressType); address.setStreet(this.street); address.setHouseNumber(this.houseNumber); address.setZipCode(this.zipCode); address.setCity(this.city); address.setEmail(this.email); address.setVerified(this.verified); address.setHistoric(this.historic); return 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 unterschiedlicher 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. Und der Builder muss zu Beginn auch gar nicht alle Attribute unterstützen. Es genügt ja, Setter-Methoden für die momentan benötigten Attribute anzubieten. Nach und nach kann der Testobjekt-Builder dann erweitert werden.