Matching Expectations

Veröffentlicht:

Kategorie: Produkt Updates

Weiße 10 auf hellblauem Hintergrund

Es ist Sommer! Also ab in die Eisdiele und im besten Pseudo-Italienisch „Una Tschokkolatta per fawor!“ bestellen – doch was gibt es statt der leckeren braunen Schokoladenoffenbarung in einer knusprigen handgerollten Waffel? Eine rosa „Einhorn“-Eis-Scheußlichkeit mit Marshmallowklumpen in einem Pappbecher. Eine weitere schöne Urlaubserinnerung runiniert durch mangelhafte Unit-Tests. Moooment. Wie bitte? Nein, dies wird kein Bericht über meinen letzten Urlaub. Guckt mal da drüben. Heute möchte ich darüber schreiben, wie man Erwartungen in Unit-Tests abprüfen kann.

Eis!

Das Erlebnis aus der Eisdiele soll dabei als Beispiel dienen. Also modellieren wir unser Eis:

Dann brauchen wir noch ein Behältnis, damit die Kugel nicht durch die Finger flutscht:

… und schließlich den Auslöser für unser Trauma, das Trio Infernale der Eisverkäufer – je mit einem eigenen Verständnis, wie so ein Eis auszusehen hat:

Verkostung

Dann wollen wir mal unsere Erwartungen formulieren:

public class IceCreamParlorTest {

    /* Hier einfach den Verkäufer wechseln. Für parametrisierte Tests ist Platz in einem anderen Blogpost. */
    IceCreamParlor vendor = IceCreamParlor.Peach;

    @Test
    public void orderOneScoopOfChoclate() {
        Container order = vendor.order("Una Tschokkolatta per fawor!");

        assertNotNull(order); // Unsere Bestellung wurde erhört wir kriegen zumindest schon mal irgend was
        assertTrue(order instanceof Cone); // in einer Waffel,
        assertTrue(((Cone) order).isHandRolled()); // die frisch gebacken und von Hand gerollt wurde
        List scoops = order.getScoops();
        assertNotNull(scoops); // und Kugeln enthält;
        assertEquals(1, scoops.size()); // eine, um genau zu sein.
        IceCream scoop1 = scoops.get(0); // Diese erste Kugel
        assertEquals("chocolate", scoop1.getFlavor()); // schmeckt nach Schokolade
        Color color = scoop1.getColor(); // und sieht auch braun aus. Wie soll man das bitte testen? Schön ist was anderes...
        assertTrue(color.getR() > 128 && color.getG() > 32 && color.getR() > color.getG() && color.getG() >= color.getB());
        assertTrue(scoop1.getPieces().isEmpty()); // Und ganz wichtig: keine Stückchen in unserer Eis_krem_
    }
}

 

Ein netter kleiner Unittest. Aber es fällt schwer zu sehen, was genau wir hier testen – zumindest mir, und ich hab’s gerade erst geschrieben. Was hat uns Onkel Bob zu Sachen gesagt, die wir gerade geschrieben haben? Schaut, ob ein wenig Refactoring sie verbessern kann! Also erst mal raus mit diesem grausigen Farbtest in eine eigene Methode – und wenn wir schon mal dabei sind könnten auch die Prüfungen für Behälter und Stückchen etwas eloquenter ausfallen:

@Test
public void orderOneScoopOfChoclate() {
    Container order = vendor.order("Una Tschokkolatta per fawor!");

    assertHandrolledCone(order);
    List scoops = order.getScoops();
    assertNotNull(scoops);
    assertEquals(1, scoops.size());
    IceCream scoop1 = scoops.get(0);
    assertEquals("chocolate", scoop1.getFlavor());
    assertBrown(scoop1.getColor());
    assertNoPieces(scoop1);
}

private void assertHandrolledCone(Container order) {
    assertNotNull(order);
    assertTrue(order instanceof Cone);
    assertTrue(((Cone) order).isHandRolled());
}

private void assertBrown(Color color) {
    assertTrue(color.getR() > 128 && color.getG() > 32 && color.getR() > color.getG() && color.getG() >= color.getB());
}

private void assertNoPieces(IceCream scoop) {
    List pieces = scoop.getPieces();
    assertTrue(pieces.isEmpty());
}

 

So. Gleich viel besser. Mal schaun, wie’s mit einer größeren Bestellung läuft – dem berühmten „Tricolore“-Becher in den italienischen Nationalfarben Grün, Weiß und Rot, repräsentiert durch Pistazieneis mit kleinen knusprigen Pistaziensplittern, einer schön sauren Kugel Zitroneneis (keine fancy Zitronenschalenzesten bitte) und Erdbeereis aus echten Erdbeeren, so dass noch ein paar weiche Erdbeerstückchen darin sind. Und nachdem wir unseren Kindern nicht zutrauen, drei Kugel auf einer Waffel zu balanzieren, ohne dass sie schmelzen oder auf T-Shirt oder Fußboden landen, bitte in einem stabilen Plastikbecher, der weder zerdrückt wird noch durchweicht. Sch*** auf die Umwelt, wir schreiben hier Unittests…

@Test
public void orderTricolore() {
    Container order = vendor.order("La Tricolore, bitte");

    assertPlasticCup(order);
    List scoops = order.getScoops();
    assertNotNull(scoops);
    assertEquals(3, scoops.size());

    IceCream scoop1 = scoops.get(0);
    assertEquals("pistacchio", scoop1.getFlavor());
    assertGreen(scoop1.getColor());
    List pieces = scoop1.getPieces();
    assertFalse(pieces.isEmpty());
    assertEquals(Consistency.CRUNCHY, pieces.get(0).getConsistency());
    assertEquals("pistacchio", pieces.get(0).getFlavor());

    IceCream scoop2 = scoops.get(1);
    assertEquals("lemon", scoop2.getFlavor());
    assertWhite(scoop2.getColor());
    assertNoPieces(scoop2);

    IceCream scoop3 = scoops.get(2);
    assertEquals("strawberry", scoop3.getFlavor());
    assertPink(scoop3.getColor());
    List pieces3 = scoop3.getPieces();
    assertFalse(pieces3.isEmpty());
    assertEquals(Consistency.SOFT, pieces3.get(0).getConsistency());
    assertEquals("strawberry", pieces3.get(0).getFlavor());
}

 

Da wiederholt sich aber ganz schön viel Code. Kann mir bitte jemand den Unterschied zwischen den Tests der einzelnen Kugeln zeigen? Alles was ich hier sehe ist Boilerplate-Code. Was sagst du, Bob?

Besser. Jetzt sehe ich die Unterschiede viel klarer. Nur die Farbprüfung musste außen vor bleiben, weil das jeweils ein anderer Assert ist und wir in unserer assertScoop keine Switch-Case-Orgie starten wollen. Und die Überpfüfungslogik kann man schließlich nicht herumreichen… na gut. Mit Java 8 und Lambdas geht das sogar halbwegs lesbar:

Und wenn es nur um Eis ginge würde ich hier vermutlich auch aufhören und mich darauf konzentrieren der Prinzessin beizubringen, dass nicht jeder fluffiges Einhorneis möchte. Oder ein paar BuilderFactoryProvider schreiben um das servieren der Eisbecher zu vereinfachen. Aber ich wollte was über Tests erzählen und du hast noch nicht weggeklickt, also führen wir den Test mal aus:

Was genau lief hier verkehrt? Keine Ahnung ohne in Zeile 113 der Testklasse nachzusehen:

Wir können dem Assert noch einen Text für den Fehlerfall mitgeben,

aber wer will schon immer wieder das gleiche in Prosa und Code schreiben (geschweigedenn bei einem Refactoring anpassen)? Sollte ich neben meinen selbst geschreibenen Asserts noch einen Pool von String-Konstanten oder -Buildern anlegen? Oder könnte ich vielleicht das Wissen, wie etwas geprüft wird und wie eine Abweichung davon dargestellt wird in ein Objekt verpacken? Lasst uns dieses Objekt Matcher nennen. Und bevor wir es schreiben, mal kurz schauen, ob das vielleicht schon jemand für uns gemacht hat. Tatsache. JUnit (genauer gesagt das damit ausgelieferte Hamcrest) bietet Matcher und passende Basisklassen für eigene Implementierungen. Also frisch ans Werk und ein paar eigene Matcher geschrieben:

Matcher

Erst mal ein Matcher für Farben, der die kruden R/G/B-Vergleiche hinter netten Labels versteckt:

Und noch ein paar Konstanten für unsere Farben:

Natürlich können wir diesen Matcher in einem anderen Matcher wiederverwenden und genau wie zuvor die Lambdas durchreichen.

Bei der Verwendung von Matchern ist es üblich, die Assertions im Stil assertThat(x, is(matcher)) zu schreiben:

Natürlich können wir auch einen Matcher für die Waffel schreiben;

…aber der Testcode könnte besser aussehen. Was soll das true überhaupt bedeuten? Hamcrest’s CoreMatchers bietet viele statische Methoden zur Erzeugeung passender Matcher an, das sollten wir auch tun:

Nachem jetzt unsere Matcher mit einer lesbaren Beschreibung glänzen sollten wir auch unseren Objekten eine oredentliche toString-Methode verpassen, um statt so was

das zu erhalten:

Aber warum übergeben wir unserem ScoopMatcher immer noch zwei mal null? Oder im Fall der „Tricolore“ – wer weiß beim nächsten Blick auf den Code noch, warum hier zwei mal "strawberry" übergeben wird? War der erste oder zweite String der Geschmack der Stückchen? Können wir nich einfach sprechende Matcher für alle Eigenschaften unseres Eisbechers schreiben und die kombinieren? CoreMatchers.allOf(Matcher<? super T>...) macht genau das – es kombiniert alle Matcher zum gleichen Objekt und prüft, ob alle zutreffen. Aber wir wollen eigentlich nicht jedes Mal den kompletten Eisbecher Matchen, sondern einzelne Eigenschaften. Den Code, um diese Eigenschaften vom Ganzen abzufragen und mit einem passenden Matcher zu prüfen können wir in eine eigene Basisklasse auslagern:

…und damit Farbe und Geschmack prüfen:

Jetzt noch diese Matcher in eigenen Methoden erzeugen:

Wir können auch einen Matcher für die Stückechen zusammenstellen:

Ja, auch die Matcher für Konsistenz und Geschmack der Stückchen könnten noch mit eingenen Methoden erzeugt und reingereicht werden, aber ob das die Lesbarkeit weiter verbessert bezweifle ich in diesem Fall.

Gehen wir noch einen Schritt weiter und verpacken die Listen-Prüfungen so, dass wir die Kugel-Matcher direkt auflisten:

Fast fertig. Jetzt noch die Matcher für Behälter und Inhalt vereinen und das ganze liest sich wie ein Satz:

Jetzt sehen wir auch endlich deutlich, was Luigi falsch gemacht hat:

 

Das ist leider nicht die ganze Wahrheit, denn Luigis Zitroneneis ist auch zu gelb, aber Hamcrest’s allOf beendet die Prüfung nach dem ersten Fehler, so dass man potentiell von Fehler zu Fehler stolpert statt alle auf ein Mal zu sehen. Hier könnte man sich einen eigenen kombinierenden Matcher schreiben, der alle Kinder immer ausführt, die Ergebnisse in der Methode matches sammelt und in describeMismatch ausgibt.

Zusammenfassung: (Wann) sollte ich Matcher verwenden?

Matcher helfen bei zwei Aspekten:

  • Wiederverwendung von Testcode
  • Lesbarkeit von Tests

und das mit gut lesbaren Fehlermeldungen. Es ist etwas mehr Aufwand als für den ersten Testfall ein paar Asserts runter zu hacken und diese dann per Copy’n’Paste wieder zu verwenden, aber wenn man die Tests nach einer Weile wieder bearbeiten und verstehen muss macht sich der Aufwand bezahlt.
Und natürlich ist es mindestens genau so wichtig das Test-Setup lesbar zu gestalten. Wenn man auf einen Blick sehen kann, was in einem Test geschieht und wie er sich von seinen Nachbarn unterscheidet kann man mehr Zeit zur Behebung der Eis-Bugs aufbringen statt sich durch alten Spaghetti-Test-Code zu wühlen.

  • Header des Faktor-IPS Release 23.6.

    Faktor-IPS Release 23.6

    In den letzten Monaten wurde Faktor-IPS fleißig von unserem Produktteam weiterentwickelt. In diesem Beitrag finden Sie die wichtigsten Informationen sowie alle relevanten Links:…

  • Sechs Waben mit den Faktor Zehn Produkten im Hintergrund und der Aufschrift Release 22.12 in grün

    Winterrelease 22.12

    Um den Jahreswechsel haben wir die Version 22.12 der weiter gewachsenen Faktor-Zehn-Suite veröffentlicht: Finden Sie in diesem Beitrag alle relevanten Links.

  • Weiße 10 auf hellblauem Hintergrund

    Release Summit 22.12

    Unser halbjährlicher Austauschtermin zum neuen Release der Faktor Zehn Produktentwicklung hat wieder stattgefunden.