Mule-Anwendungen mit MUnit testen (Teil 3): Tabellenbasierte Tests

Keine Kommentare

Am Ende des zweiten Teils hatte ich schon den Begriff „tabellenbasierte Tests“ erwähnt. Was heißt das? Es geht darum, mehrere gleichartige Tests auszuführen, die sich nur durch Eingabe- und erwartete Ausgabedaten unterscheiden.

Das Schöne an dieser Art der Tests ist, dass die Tabellen mit den Testdaten auch für weniger IT-affine Personen lesbar sind. Der Fachbereich kann somit nachvollziehen, was im Test passiert oder sogar Testdaten liefern. In unserem Beispiel – dem Body-Mass-Index-Rechner – könnte eine Tabelle mit Testdaten folgendermaßen aussehen:

Größe Gewicht BMI Beschreibung
1.88 79 22.35 Normalgewicht
1.88 90 25.46 Präadipositas
1.88 150 42.44 Präadipositas Grad III
1.88 50 14.15 starkes Untergewicht

Die Eingabedaten befinden sich in den ersten beiden Spalten (Größe in Metern, Gewicht in Kilogramm), die erwarteten Ergebnisse in den Spalten drei und vier (BMI, zwei Nachkommastellen, Beschreibung).

Die Spalten sind durch Tabulatoren getrennt (im Layout nicht erkennbar). Die Aufgabe besteht nun darin, für jede Zeile den BMI-Rechner aufzurufen und das Ergebnis zu überprüfen.

Welche Aufgaben müssen wir für den Test abarbeiten? Die CSV-Datei muss gelesen und „zerlegt“ werden (Parsing). Anschließend folgt dann in einer Schleife die gleiche Arbeit, die wir im letzten Teil auch hatten: Daten für einen Aufruf zusammenstellen, Service aufrufen, Ergebnisse überprüfen. Fangen wir an…

Einlesen und parsen

Gemäß Maven-Konventionen gehört die Tabelle mit den Testdaten nach src/test/resources, ich habe noch ein Unterverzeichnis (alias Package) data_tables spendiert. Wer erinnert sich noch daran, wie im ersten Teil die XML-Daten für den SOAP-Aufruf in die Message kamen? Set Message aus der MUnit-Palette ist die korrekte Antwort. Darin steht dann der folgende MEL-Ausdruck:

#[getResource('data_tables/bmi.csv').asStream()]

Als Ergebnis steht ein InputStream mit der CSV-Datei in der Nachricht. Nicht vergessen: Das Encoding sollte noch passend eingestellt werden (z.B. UTF-8, oder Cp1252 bei Daten vom Fachbereich), außerdem darf der Mime-Type (application/csv) nicht fehlen.

Das CSV-Parsen erledigt DataWeave mit einem sehr simplen Script:

%dw 1.0
%output application/java
---
payload

Dass in dem Stream CSV steht, weiß DataWeave bereits durch den Mime-Type, aber woher weiß DataWeave, wie das CSV aussieht? Das ist über die Reader Configuration möglich, die man per rechtem Mausklick auf die Payload erreicht:

reader-configuration

Quote benötigt man dann, wenn innerhalb einer Zelle das Trennzeichen oder ein Zeilenumbruch steht, Ignore Empty Line ist selbsterklärend. Der Haken bei Header bewirkt, dass die erste Zeile als Überschrift erkannt wird. Escape wird innerhalb einer Zelle dem Quote-Zeichen vorangestellt, falls es in der Zelle vorkommt. Schwierig wird es beim Separator: Wie gibt man ein Tab-Zeichen ein? Ich habe es in der Oberfläche nicht hinbekommen und habe (mal wieder) die XML-Ansicht eingeschaltet:

<dw:input-payload mimeType="application/csv" doc:sample="list_csv.csv">
  <dw:reader-property name="separator" value="&#009;" />
</dw:input-payload>

Wer den ASCII-Code im Kopf hat: Für den Tabulator steht die 9. Die anderen Attribute fehlen im XML, weil sie auf den Default-Werten stehen.

Über den Rechtsklick auf die Payload lassen sich auch noch die Meta-Daten der CSV-Datei definieren, entweder manuell oder über eine Beispieldatei:
csv-metadata

Macht man sich die Mühe mit den Meta-Daten, kann man in einem folgenden DataWeave Attribute per Drag and Drop einfach von links nach rechts ziehen und spart sich das manuelle Tippen. Aber auch beim Tippen wird die Welt einfach: Control-Space-Completion funktioniert ebenso auf Basis der Meta-Daten. Hier kommen wir aber auch ganz gut ohne diese Information aus.

Was steht nun nach dem DataWeave mit dem simplen Script payload in der Message? Eine ArrayList von Maps, letztere mit Spaltennamen als Key und den Zellinhalten als Value. Damit haben wir eine Struktur, über die sich mit For Each iterieren lässt.

Vorbereiten und aufrufen

Innerhalb der Schleife enthält die Message jetzt die eben genannte Map, aus der wir die Testdaten für den SOAP-Aufruf vorbereiten müssen. Der Übersichtlichkeit halber habe ich das in einen Sub-Flow ausgelagert:
prepare-row

Im ersten Schritt wird per Set Variable die Payload in die Variable row gesichert. Das ist notwendig, da der nächste Schritt die Payload ersetzt und wir den Inhalt noch für die später folgende Verifikation benötigen.

Die SOAP-Aufrufe innerhalb der Schleife sind gleichartig: Das XML ist bis auf die Werte für Körpergröße und Gewicht immer gleich. Um es zu erzeugen, bietet Mule Parse Template, eine minimalistische Template-Engine (ohne Bedingungen und Schleifen), die in einem Text MEL-Ausdrücke durch deren Auswertungsergebnis ersetzen kann. Das Template ähnelt damit dem konstanten XML aus dem vorigen Teil der Serie:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                  xmlns:bmi="http://codecentric.de/services/bmi">
  <soapenv:Header/>
  <soapenv:Body>
    <bmi:calculateBmi>
      <input>
        <length>#[payload.Grösse]</length>
        <mass>#[payload.Gewicht]</mass>
      </input>
    </bmi:calculateBmi>
  </soapenv:Body>
</soapenv:Envelope>

Statt payload hätte ich übrigens auch row schreiben können, da wir die Payload in eine Flow-Variable kopiert haben und die Flow-Variablen direkt im Scope für MEL-Ausdrücke stehen.

Der Aufruf des Service geschieht – wie bei MUnit üblich – über eine Flow Reference. Auch hier hat sich gegenüber dem letzten Teil der Serie nichts geändert.

Verifizieren

Der SOAP-Aufruf gibt ein XML zurück. Dessen Überprüfung läuft schrittweise ab. Da die ersten drei Schritte für diesen Testfall gleich sind wie für den Testfall aus dem letzten Teil der Serie, habe ich sie in einen Sub Flow ausgelagert. Schließlich gilt auch in Tests: Don’t repeat yourself.

Zur Erinnerung: Der erste DataWeave streift den SOAP-Envelope ab, der Filter prüft die Struktur des XMl-Response gegen seine XSD, der zweite DataWeave extrahiert die beiden relevanten Teile aus dem Ergebnis (bmi, description) in gleichnamige Flow-Variablen.

Die folgenden Asserts stehen wieder in einem Sub Flow:
assert-row

Die Ausdrücke greifen auf die Flow-Variable row zurück, in der unsere Tabellenzeile als Map steht. Daraus ergibt sich das erwartete Ergebnis (expectedValue), das mit dem Ergebnis des Aufrufs (actualValue) verglichen wird:

 <munit:assert-on-equals message="bmi" 
          expectedValue="#[row.BMI]" 
          actualValue="#[bmi]" 
          doc:name="Assert bmi" />
 <munit:assert-on-equals message="description" 
          expectedValue="#[row.Beschreibung]" 
          actualValue="#[description]" 
          doc:name="Assert description" />

Zusammengesetzt ergeben die Einzelteile das folgende Gesamtbild:
table-based-test

Es beginnt mit dem Lesen/Parsen der CSV-Datei. Dann folgt die Schleife, in der nur noch Sub Flows aufgerufen werden. Ich habe das alte Motto Caesars beherzigt: Teile und herrsche. Analog zur Programmierung (z. B. in Java) sollten die Einzelteile (z. B. Methoden) klein und übersichtlich sein.

Zusammenfassung und Ausblick

Vergleichen wir den hier gezeigten tabellenbasierten Testfall mit dem einfachen Testfall aus dem letzten Teil der MUnit-Serie: Was hat sich geändert? Gar nicht so viel: Einige Teile sind in Sub Flows gewandert, was man zwecks Übersichtlichkeit auch schon vorher hätte machen können. Einige dieser Sub Flows  sind jetzt auch über Flow-Variablen konfigurierbar, was im ersten Teil nicht notwendig war. Ansonsten sind die Änderungen – und damit der Mehraufwand – eher gering: Zwei Schritte zum Lesen und Parsen der CSV-Datei sowie die Schleife (For Each) sind dazugekommen. Statt die XML-Testdaten direkt zu lesen, kommt das Mule-Element Parse Template zum Einsatz.

Was haben wir gewonnen? Wir haben mehrere Testfälle mit gleichartiger Struktur ohne großen Aufwand und vor allem ohne Copy-and-Paste-Orgien umgesetzt. Zusätzlich haben wir den Testcode von den Testdaten getrennt. Als Nebeneffekt liegen die Testdaten in einem Format vor, das man als Austauschformat mit dem Fachbereich nutzen kann.

Im nächsten Teil der Serie werde ich mich dann um das Mocking von Konnektoren kümmern. Dafür brauche ich dann aber ein neues Beispiel, beim BMI-Rechner war schließlich nichts zu mocken.

Links

Roger Butenuth

Dr. Roger Butenuth hat in Karlsruhe Informatik studiert und anschließend in Paderborn promoviert (Kommunikation in Parallelrechnern). Er hat langjährige Erfahrung in der Projekt- und Produktentwicklung.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

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