Beispielmethode

Beginnen wir mit dem folgenden Beispiel eines Integrationstests:

@Test
void getRemoteRequests() throws Exception {
    assertSuccessfulGetId(X_CORRELATION_ID, X_REQUEST_ID);

    MvcResult mvcResult = runGetRemoteRequests(X_CORRELATION_ID, true);

    assertThat(mvcResult).hasStatusCode(HttpStatus.ACCEPTED);
    assertRequestResponse();
  }
}

Diese Testmethode ist Teil eines Microservices, der mit einem Drittsystem kommuniziert und selbst verschiedene Operationen anbietet, darunter unter anderem getId und getRemoteRequests, welche in unserem Beispiel relevant sind.

Im Kontext des Integrationstests führen wir in assertSuccessfulGetId die Operation getId aus und stellen sicher, dass diese erfolgreich war. Was getId genau macht und wofür die Id genutzt werden kann, ist in diesem Zusammenhang irrelevant – relevant ist in dieser Operation nur der Aufruf an das Drittsystem. Ziel von getRemoteRequest ist es nämlich, alle zuvor ausgeführten Requests an das Drittsystem zurückzugeben. Bei Ausführung von getRemoteRequest erhalten wir in diesem Test also ein JSON-Objekt zurück, welches den Request an das Drittsystem von getId enthält.

In dem Test wollen wir nun sicherstellen, dass der Rückgabewert von getRemoteRequest unseren Erwartungen entspricht. Wir müssen also sicherstellen, dass der Request in der Rückgabe wirklich so aussieht, wie ein üblicher Request, der von getId an das Drittsystem geschickt wird.

Dieser Vorgang lässt sich über verschiedene Wege realisieren, ist jedoch mit einem hohen Aufwand verbunden.

Validation File Assertions

Hier kommen die Validation File Assertions ins Spiel. Sie konvertieren Assertions in Textform, um diese dateibasiert schneller und übersichtlicher überprüfen zu können. Ein großer Vorteil ist dabei, dass mehrere Assertions in einem Schritt ausgeführt werden können, was wiederum den Aufwand bei der Erstellung von Tests minimiert.

Darüber hinaus führt die Validation File Assertion nicht nur den Check der Assertion an sich aus, sondern zeigt diese direkt auch im richtigen Kontext.

In unserem Beispiel enthält die Validation-File-Textdatei den Request und Response von getRemoteRequest:

Beispiel einer Validation-File-Textdatei
In der Abbildung sieht man den request von getRemoteRequest und in der Antwort den request von getId, so wie gewünscht.

Validation File Assertions durchlaufen bei der Ausführung des Tests folgende Etappen:

Beispiel einer Validation-File-Textdatei
  1. Das Testergebnis wird zunächst in eine Textdatei in den Ordner „output“ geschrieben.

  2. In einem zweiten Ordner „validation“ wird überprüft, ob die Datei bereits vorhanden ist.

  3. Falls die Datei dort noch nicht vorhanden ist, wird auch sie automatisch erstellt und mit einem Marker „neue Datei“ versehen. Ist sie bereits vorhanden, kann der eigentliche Check direkt starten (siehe Punkt 6).

  4. Der Entwickler sichtet die automatisch erstellte Datei im Ordner „validation“ einmalig und überprüft, ob sie den Erwartungen entspricht und als Referenz dienen soll.

  5. Ist dies der Fall, kann der Marker entfernt und als korrektes Referenz-Ergebnis des Tests gewertet werden.

  6. Bei jeder erneuten Ausführung des Tests wird die neue Datei mit der freigegebenen Referenzdatei verglichen.

  7. Nun sind zwei verschiedene Ergebnisse möglich: Sind die Dateien identisch, gilt der Test als bestanden; sind die Dateien hingegen unterschiedlich, schlägt der Test fehl.

cronns IntelliJ-Plug-in

Um Entwicklern die Überprüfung von Validation Files zu erleichtern, hat cronn ein IntelliJ-Plug-in entwickelt. Durch das Plug-in wird die Überprüfung noch einfacher, da es die Unterschiede der Validation Files anzeigt.

Anhand des zu Beginn genannten Beispiels können wir sehen, wie ein fehlgeschlagener Validation-Files-Vergleich mit dem Plug-in aussieht:

fehlgeschlagener Validation-Files-Vergleich
Auf der linken Seite der Abbildung ist die Datei zu sehen, die beim aktuellen Testdurchlauf erzeugt wurde, rechts ist die Vergleichsdatei aus dem validation-Ordner abgebildet.

Welcher Code versteckt sich hinter den Validation File Assertions?

Kommen wir zu unserem Beispiel vom Anfang zurück:

Hinter der letzten Zeile versteckt sich der Aufruf des Open-Source-Projekts von cronn, welches besagten Vergleich der Validation Files durchführt. Im Detail ruft assertRequestResponse die folgende Methode compareActualWithFile auf:

package de.cronn.assertions.validationfile.util;

public final class FileBasedComparisonUtils {

    public static void compareActualWithFile(String actualOutput, String filename, ValidationNormalizer normalizer) {
        String fileNameRawFile = filename + ".raw";
        writeTmp(actualOutput, fileNameRawFile);
        String normalizedOutput = normalizer != null ? normalizer.normalize(actualOutput) : actualOutput;
        String normalizedActual = normalizeLineEndings(normalizedOutput);
        prefillIfNecessary(filename, normalizedActual);
        String expected = readValidationFile(filename);
        writeOutput(normalizedActual, filename);
        assertEquals(expected, normalizedActual, filename);
    }

}

Die erwarteten Parameter sind in diesem Fall das zu vergleichende Objekt als String, der Dateiname, ebenfalls als String, und ein sogenannter „Normalizer“.

In unserem Beispiel haben wir Requests und Responses als String aufgezeichnet. Dies ist hier unser zu vergleichendes Objekt. Grundsätzlich kann jedes Objekt als String in die Methode gereicht werden. Dies kann man z.B. mit der typischen toString-Methode erreichen.

Für die Generierung der Dateinamen gibt es zwei Möglichkeiten: Die Datei kann sowohl projektbezogen individuell erstellt oder automatisiert festgelegt werden.

Normalizer sind ein zentraler Bestandteil der Validation File Assertions. Unter Umständen können sich Teile des zu vergleichenden Objekts bei jedem Testdurchlauf verändern. In unserem Beispiel vergleichen wir Requests. Diese haben eine einzigartige requestId, welche sich bei jedem Testdurchlauf unterscheidet. Die Validation File Assertions würden demnach bei jeder Testausführung fehlschlagen. Hier kommen Normalizer ins Spiel: Sie ermöglichen, projektspezifisch veränderliche Informationen zu maskieren und dadurch das Fehlschlagen des Tests zu verhindern. Im näheren Verlauf folgt dazu ein weiteres Beispiel.

Konkretes Vorgehen

In der Methode compareActualWithFile wird nun zunächst eine temp-Datei“ mit dem individuell oder automatisiert erstellten Dateinamen generiert. Danach kommen die Normalizer zum Einsatz, die die gewünschten veränderlichen Informationen maskieren.

Falls die Datei bisher noch nicht vorhanden ist, erstellen wir sie mit der Methode prefillIfNecessary im Validation-File-Ordner und versehen sie mit dem Marker „neu“. Danach lesen wir entweder die neu erstellte Datei oder die bereits existierende aus. Unsere maskierte Datei speichern wir im „output“-Ordner ab und vergleichen sie mit der anderen Datei aus dem „validation“-Ordner.

Wir sehen also in dieser Methode genau den Ablauf, der im obigen Schaubild verdeutlicht wurde.

Für die Normalizer liefert unser Projekt das Objekt ValidationNormalizer. Diese sehen dann im einfachsten Fall basierend auf einer Regular Expression (Regex) so aus:

protected ValidationNormalizer requestIdNormalizer() {
  return s ->
      s.replaceAll(" " + RequestIdGenerator.PREFIX + "\\d+", " [generated-for-request]");
}

Der Effekt dieses Normalizers sieht in unserem Beispiel wie folgt aus:

Normalizer Effekt

Der Kreativität sind bei der Erstellung dieser Normalizer keine Grenzen gesetzt – die maskierenden Stellen lassen sich ebenfalls durch andere Vorgehensweisen finden. Der besseren Verständlichkeit halber ist hier jedoch der einfachste Fall abgebildet.

Fazit

Validation Files ermöglichen Entwicklern auf unkomplizierte Weise, komplexe Objekte miteinander zu vergleichen. Obwohl die Überprüfung der Datei(en) dennoch konzentriertes Lesen der Entwickler erfordert, ersparen die Validation Files gleichzeitig das komplizierte und vor allem langwierige Schreiben von einzelnen Assertions. Mittels Validation Files kann außerdem ein wichtiger Aspekt des Regressionstests abgedeckt werden: Implizit werden zahlreiche Informationen mitgetestet. So wird zum Beispiel der Request-Aufbau in unserem Beispieltest mitüberprüft – Änderungen in der API würden dadurch sofort auffallen.

Nicht zu vernachlässigen ist außerdem die Geschwindigkeit von Validation Files. Da sie auf einfachen Textdateien basieren, kann die Performance außer Acht gelassen werden. Veränderliche Informationen müssen zwar maskiert werden, jedoch kann auch dieser Prozess durch die bereitgestellten Objekte projektspezifisch schnell erledigt werden.

In welcher Form man Validation File Assertions in der Praxis einsetzt, hängt vom Umfang der jeweiligen Komponenten ab. Bei geringem Umfang kann es weiterhin sinnvoll sein, einfache Assertions zu nutzen. Validation File Assertions können jedoch gerade bei komplexen Objekten das Testen durch die Reduzierung des Schwierigkeitsgrads deutlich erleichtern.