Approval Testing

Das folgende Szenario sollte jedem Software Entwickler nicht ganz
unbekannt sein: Ein Projekt ist beendet und du wechselst in ein neues
Projekt. Wobei neu nicht immer so neu sein muss…

Es kann auch sein …

  • Das einzige Neue im Projekt bist du.
  • Das Projekt läuft schon seit Jahren produktiv.
  • Deine Aufgabe ist es, Support für diese Projekt zu machen. Dies kann in der Form von Bugfixing oder neuen Features sein.
  • Und folgendes hätte ich fast vergessen: es gibt keine (Unit) Tests.

Was tun?

Nun, die Antwort wäre eigentlich ganz klar: auf der grünen Wiese Tests schreiben, bis man eine gute Testabdeckung hat. Das Problem ist nur: der Weg dahin ist lange und das will der Kunde sicherlich nicht bezahlen.

Ein neuer Ansatz

Genau hier setzt Approval Testing ein. Wir gehen davon aus, dass die (zu testende) Software in der aktuellen Version ein korrektes Verhalten hat. Dies ist ja dadurch gegeben, dass die Software schon lange Zeit in der Produktion läuft. Wir schreiben Tests, die den Code aufrufen … aber wir schreiben keine Asserts. Die Asserts werden “automatisch” generiert, indem das Resultat des Tests einmal gespeichert wird und bei jedem nachfolgenden Aufruf des Tests gegen dieses Resultat geprüft wird. Sehen wir uns das einmal an einem Beispiel an:

public class RatingCalculator {

    public int rate(String firma) {
        // ... legacy code ...
        if (firma.equals("Puzzle")) {
            return 5;
        } else {
            return 0;
        }
    }
}

Das “neue” Projekt ist ein RatingCalculator, der für einen Firmennamen ein Rating berechnet. Der Code ist hier stark vereinfacht. In wirklichen Projekten kann der Aufruf von rate() tausende Zeilen von Code ausführen. Und für diese rate() Methode möchten wir nun einen Approval Test schreiben. Es gibt diverse Frameworks, die dies (in verschiedenen Programmier-Sprachen) unterstützen. Wir verwenden hier https://approvaltests.com für die Sprache Java. Mittels Maven können wir uns die Framework Klassen herunterladen.

<dependencies>
    <dependency>
        <groupId>com.approvaltests</groupId>
        <artifactId>approvaltests</artifactId>
        <version>9.5.0</version>
    </dependency>
</dependencies>

Nun schreiben wir den eigentlichen Test.

import org.approvaltests.Approvals;
import org.junit.Test;

public class RatingTest {

@Test
public void puzzle_rating_is_excellent() {
    RatingCalculator calc = new RatingCalculator();
    int rating = calc.rate("Puzzle");
    Approvals.verify(rating);
}

Wir rufen den Calculator auf und merken uns das Resultat. Je nach Typ des Resultats müssen wir dieses in ein “approval” kompatibles Format konvertieren. Was heisst das genau?

Das “approval” kompatible Resultat muss folgende Kriterien erfüllen:

  • es muss nach jedem Aufruf gleich aussehen und in einer für Menschen lesbaren Form sein.
  • Nicht konstante Zeit-, Datums- oder Zählerwerte sind zu vermeiden.
  • Ein komplexes Businessobjekt mittels toString() in einen String umzuwandeln (ohne toString() zu überschreiben) ist keine gute Idee.
  • Es sollte in einer für Menschen lesbaren Form sein, ansonsten wird es später schwierig zu verstehen, warum ein Test fehlgeschlagen ist. Bei einem Integer ist das einfach so gegeben. Für komplexere Resultate bietet sich eine Umwandlung in ein JSON oder XML String an.

Mittels Approvals.verify(rating) prüfen wir, ob das Resultat stimmt. Da wir den Test zum ersten Mal starten, wird er rot sein. Es wird aber vom Approval Framework ein Textfile erstellt, welches das erhaltene Resultat enthält. Der Filename hat die Form:

Name-der-Testklasse.Names-des-Tests.received.txt

Für unseren Fall sieht das dann so aus:

RatingTest.puzzle_rating_is_excellent.received.txt

Wenn wir nachschauen ist dort der Werte “5” gespeichert. Dieses File muss man nun manuell umbenennen in “approved”:

RatingTest.puzzle_rating_is_excellent.approved.txt

Dann starten wir den Test nochmals. Der Test vergleicht das Resultat des Tests mit dem “approved” Resultat und der Test sollte nun grün sein.

Was haben wir nun erreicht?

Mit einem einzigen “kleinen” Test haben wir eine gute Testabdeckung der Businesslogik. Damit kann man in kleinen Schritten mit einem Refactoring des Legacy Codes beginnen und zugleich selbst konventionelle jUnit Test schreiben … aber immer mit der Sicherheit der Approval Tests im Rücken.

Und was passiert wenn wir einen Fehler einbauen? Ersetzen wir als Beispiel in rate() den “Puzzle” Rückgabewert von 5 auf 2. Wenn wir den Test nochmals laufen lassen, wird der Test rot und es gibt eine Fehlermeldung inLog. Je nach (Approval) Konfiguration öffnet sich zusätzlich ein DiffTool und zeigt uns die Differenz an.

Fazit

Approval Testing ist eine Möglichkeit in einem Projekt die Testabdeckung schnell und massiv zu erhöhen:

  • Je nach dem wie kompliziert die Resultate sind, muss Zeit in die “Serialisierung” in einen String investiert werden. Je lesbarer diese Repräsentation ist, desto einfacher wird es später bei roten Tests im DiffTool die Unterschied zu sehen.
  • Approval Tests sollten aber nicht der Endzustand sein, sondern der Startpunkt zu einem sicheren Refactoring und selbst erstellten jUnit Tests.

Wir habe diese Technik selbst ein einem Kundenprojekt eingesetzt und damit gute Erfahrungen gemacht. Wir konnten mit relativ wenig Zeitaufwand einen grossen Nutzen erhalten.

Links:

https://approvaltests.com

Schreibe einen Kommentar

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