Builder für Unit-Tests

4 Minuten zum lesen

Das Schreiben von Unit-Tests ist nichts besonderes. Gerade bei der testgetriebenen Entwicklung (TDD) gehört das Schreiben von Unit-Tests zur normalen Tätigkeit von Entwicklern. Egal ob man die Tests vor oder nach dem produktiven Code schreibt ist es aber immer lästig und fehleranfällig, wenn vor dem eigentlichen Test ein umfangreiches Setup durchgeführt werden muss. Auch die Auslagerung des immer gleichen Codes in eine @Before Methode hilft dabei nicht unbedingt weiter. Sehr schnell wird die setUp() Methode dann immer größer unübersichtlicher und trägt zur langen Ausführungszeit von Tests bei. Ein weiteres Problem sind Setups, die immer wieder in mehreren Testklassen vorgenommen werden. Diese Redundanz macht den Testcode sehr schwer wartbar.

Eine mögliche Lösung für diese Probleme ist die Einführung von Hilfsklassen. Hier stellt sich das Problem, dass man die Hilfsklassen entweder zu speziell und damit kaum wiederverwendbar erstellt. Oder man baut diese Klassen so generisch, dass deren Anwendung nicht viel einfacher als der reine Setup Code wird.

Ich möchte in diesem Beitrag eine andere Lösung beschrieben. In meinem aktuellen Projekt verwende ich das Builder Pattern. Mit diesem wird die Erstellung von (Entity-)Objekten, die für den Test benötigt werden vereinfacht. Durch eine Erweiterung des Patterns können neben reinen Objekten im Speicher auch Datensätze in der zugrunde liegenden (Test-)Datenbank erzeugt werden.

Eine weitere Möglichkeit zur Vereinfachung der Erstellung von Objekten stellt das Object Mother Pattern dar. Auf dieses möchte ich an dieser Stelle nicht weiter eingehen. Ein Vergleich von Builder Pattern und Object Mother Pattern findet sich hier.

Das Builder Pattern

Kurz gefasst handelt es sich bei dem Builder Pattern um ein Muster zur (einfachen) Erzeugung von (komplexen) Objekten. In Bezug auf Unit-Tests kapsle ich in einem Builder die Erstellung von ein oder mehreren Daten-Objekten. Ein Builder kann dabei nicht nur ein Objekt sondern auch eine ganze Objektstruktur (Kompositum) erzeugen.

Zur Vereinfachung erzeugt mit ein Builder ohne weitere Parameter ein “Standardobjekt”. Die einzelnen Attribute sind dabei mit Standartwerten vorbelegt. Ich habe jedoch die Möglichkeit einzelnen Attribute bei der Erzeugung anzugeben, um so ein für meine Tests maßgeschneidertes Objekt zu erhalten.

Als Beispiel sei das Erzeugen einer Adresse genannt. Ohne das Builder Pattern würde mein Code wie folgt aussehen.

Address address = new Address("Straße", "PLZ", "Ort");

Mit einem Builder, der mir ein Standard Objekt erstellt, sieht der Code wesentlich einfacher aus.

Address address = AddressBuilder.newAddress().build();

Soll im Gegensatz zur Vorgabe in anderer Ort verwendet werden, kann ich diesen vor der Erstellung überschreiben.

Address address = AddresBuilder.newAddress().withPlace("anderer Ort").build();

Der zugehörige Code des AdressBuilder sieht wie folgt aus.

public class AddressBuilder {

    private String street;
    private String zip;
    private String place;

    public static AddressBuilder newAddress() {
        return new AddressBuilder();
    }

    public Address build() {
        return new Address(street, zip, place);
    }

    public AddressBuilder withStreet(String street) {
        this.street = street;
        return this;
    }

    public AddressBuilder withZip(String zip) {
        this.zip = zip;
        return this;
    }

    public AddressBuilder withPlace(String place) {
        this.place = place;
        return this;
    }

}

Mit diesem Pattern wird die Erstellung von Setups bereits wesentlich erleichtert. Auch ist der Code besser lesbar. Eine Erweiterung um weitere Attribute bedingt evtl. nur die Ergänzung des Builders. Nur bei den Tests, bei dem das neue Attribut eine Rolle spielt muss der Aufruf des Builders angepasst werden.

Nutzung des Builder Patterns zur Erstellung von Datensätzen

In seinem Blog beschreibt Marc Philipp eine Möglichkeit mittels DBUnit und dem von ihm erstellten DataSetBuilder relativ einfach Datensätze in einer (Test-)Datenbank anzulegen. Den Weg zu dieser Lösung hat er sehr anschaulich in einer Präsentation beschrieben.

Kombinierte Builder für Objekte und Datensätze

Im aktuellen Projekt habe ich sowohl die Anforderung mittels Builder Objekte im Speicher zu erzeugen. Von den gleichen Objekten sollen für integrative Tests aber auch Datensätze erzeugt werden. Hierzu habe ich die “normalen” Builder für Unit-Tests und den DataSetBuilder für DBUnit Tests miteinander “verbunden”.

Sowohl für einen “normalen” Builder als auch für einen Builder zum Bau von Datensätzen werden die with Methoden zur Festlegung von Werten benötigt. Auch die Speicherung dieser Werte in einem Feld sind bei beiden Buildern identisch.

Unterschiedlich ist einzig und allein die Erzeugung. Bei einem herkömmlichen Builder verwende ich die Methode build um ein Objekt mit den Standard- bzw. überschriebenen Attribut-Werten zu erzeugen. Beim Erstellen eines Datensatzes lasse ich mir einen DataSetBuilder übergeben und erstelle Aufgrund der Attribut-Werte sowie der in meinem Builder definierten Spaltendefinitionen einen neuen Datensatz.

Grundlage meiner “kombinierten” Builder ist der Builder für die Objekte. Aufbauend auf diesem fügt der xxx RowBuilder die Definition der Spalten hinzu. Weiterhin wird die Methode addTo implementiert, die den Datensatz dem übergebenen DataSetBuilder hinzufügt.

Aufbauend auf dem oben beschriebenen Builder für Adressen könnte der zugehörige RowBuilder wie folgt aussehen. Dabei ist zu beachten, dass die Felder im AddressBuilder von private auf protected gesetzt werden müssen.

public class AddressRowBuilder extends AddressBuilder {

    private static final String TABLE_NAME = "ADDRESS";
    private static final ColumnSpec<String>
        STREET = ColumnSpec.newColumn("street");
    private static final ColumnSpec<String>
        ZIP = ColumnSpec.newColumn("zip");
    private static final ColumnSpec<String>
        PLACE = ColumnSpec.newColumn("place");

    public static AddressRowBuilder newAddressRow() {
        return new AddressRowBuilder();
    }

    public void addTo(DataSetBuilder dataSetBuilder)
        throws DataSetException {
        dataSetBuilder.newRow(TABLE_NAME)
            .with(STREET, street)
            .with(ZIP, zip)
            .with(PLACE, place)
            .add();
   }

}

Erfahrung

Die Erstellung aller benötigten Builder ist derzeit noch am Laufen. Danach muss dann noch die Anpassung der bestehenden Tests an die Builder erfolgen. Der hierfür notwendige Aufwand ist sicherlich nicht gering. An den bereits umgestellten Tests ist aber zu erkennen, dass die Tests verständlicher sind und auch neue Tests wesentlich schneller geschrieben werden können.