Mule-Anwendungen mit MUnit testen (Teil 4): Mocks und Spies

Keine Kommentare

Nach einem langen zweiten Teil und einer noch längeren Pause kommt heute ein kurzer Text über Mocks und Spies in MUnit Tests. Mocks machen das, was man aus JUnit kennt; was Spies machen, werden wir gleich sehen.

Um es nicht zu kompliziert zu machen, benutze ich nur einen sehr einfachen Flow für die Demo:

Einfacher Flow, der einen Webservice aufruft.

Der Flow stammt aus einem Projekt, das mit RAML (restful api modelling language) und mit dem API-Kit von Mule erstellt wurde. Das ist im Rahmen des Tests aber egal. Worauf es ankommt: Links geht ein JSON-Dokument hinein, Beispiel:

{
  "groesse": 1.8,
  "gewicht": 80
}

Rechts wieder ein JSON raus, Beispiel:

{
  "beschreibung": "Normalgewicht",
  "bmi": 24.7
}

In der Mitte steht der Web-Service-Consumer von Mule, der in Ein- und Ausgabe mit XML arbeitet. Die beiden Transformer müssen also zwischen JSON bzw. XML vermitteln und dabei noch Attribute umbenennen.

Der erste Transformer (build request) macht aus dem JSON ein XML, das zu der WSDL des SOAP-Service passt:

%dw 1.0
%output application/xml
%namespace bmi http://codecentric.de/services/bmi
---
{
  bmi#calculateBmi: {
    input: {
      length: payload.groesse,
      mass: payload.gewicht
    }
  }
}

Der zweite Transformer (build response) arbeitet in der Gegenrichtung und produziert aus dem XML wieder JSON:

%dw 1.0
%output application/json
%namespace bmi http://codecentric.de/services/bmi
---
{
  beschreibung: payload.bmi#calculateBmiResponse.return.description,
  bmi: payload.bmi#calculateBmiResponse.return.bmi as :number
}

Wie gehen wir beim Test vor? Prinzipiell existieren hier zwei Geschmacksrichtungen: eher Unit-Test, bei dem der Test für sich ausführbar ist, oder eher Integrationstest, in dem Randsysteme – hier der SOAP-Service – mitgetestet werden. Welche der beiden Varianten sinnvoller ist, soll hier nicht diskutiert werden (das ist einen eigenen Blog-Post wert).

So, jetzt geht es endlich los: Im Anypoint Studio erzeugen wir durch einen Rechtsklick auf den Flow mit MUnit -> Create new suite die leere Hülle für den Testfall:

Testhülle

Wie viele Dinge in Mule sieht der Testfall einem Flow recht ähnlich. Was anders ist:

  • Es fehlt der Listener (oder endpoint), der den Flow startet. MUnit-Tests werden durch Maven oder aus Anypoint direkt gestartet.
  • Schaut man ins XML – oder auf den Reiter „Global Elements“ – sieht man eine „MUnit Configuration“ und Spring Imports. Mit Letzeren gibt man an, welche XML-Dateien mit Flows für den Test geladen werden. Das kann von den XML-Dateien in der deploy.properties abweichen.

Den Aufruf des zu testenden Flows hat der Wizard vom Studio freundlicherweise schon direkt generiert. Wir müssen nur noch die richtigen Eingabedaten zur Verfügung stellen, den SOAP-Service mocken und die Ausgabe prüfen. So sieht es aus, wenn’s fertig ist:

Flow mit Mock und Asserts

Jetzt aber schrittweise:

Links starten wir mit einem „Set Message“, quasi der Ersatz für den http-Listener, der den Flow sonst mit Daten versorgt.

 <munit:set payload="#[getResource('input.json').asStream()]" 
            mimeType="application/json" 
            doc:name="Set Message"/>

Er lädt die JSON-Datei mit den Eingabedaten aus src/test/resources/input.json und stellt sie in einem Stream zur Verfügung. Außerdem setzt er den korrekten Mime-Type. Für unseren einfachen Flow reicht das aus. Falls der Flow noch weitere Daten aus dem http-Request benötigt: Hier lassen sich Properties aus allen vier Scopes setzen.

Mock

Der zweite Schritt ist schon der Aufruf des Flows. Ohne weitere Eingriffe würde der Flow den originalen SOAP-Service aufrufen. Da wir das verhindern wollen, ziehen wir einen Mock in den Bereich „Setup“ (im Bild oben). Der Mock sieht im XML folgendermaßen aus:

<mock:when messageProcessor=".*:.*" doc:name="Mock soap call">
  <mock:with-attributes>
    <mock:with-attribute name="doc:name" whereValue="#['call bmi-soap']"/>
  </mock:with-attributes>
  <mock:then-return payload="#[getResource('output.xml').asStream()]" mimeType="application/xml"/>
</mock:when>

Der schwierigste Teil beim Mock ist es, den richtigen Message-Prozessor zu treffen. Die Eingrenzung erfolgt nach zwei Strategien:

  1. Direkt im mock:when Tag mit dem Attribut messageProcessor. Hier zielt man per regulärem Ausdruck auf Namespace und Name des XML-Tags. Das ist leider nicht sehr zielgenau: Es kann ja durchaus mehrere SOAP-Aufrufe im Flow geben. Der Wizard im Anypoint Studio trägt an der Stelle daher nur „.*:.*“ ein, was auf alles zutrifft.
  2. Durch die Eingrenzung auf Attribute des Message Processors. Der Wizard wählt dafür standardmäßig doc:name aus, prinzipiell kann man hier aber beliebige Attribute verwenden. Da der Vergleichswert eine MEL-Expression enthalten darf, ist hier auch Dynamik möglich.

Insgesamt kann man die beiden Strategien als sehr kritisch ansehen: Durch Refactoring in den Flows wird ein Match schnell zerstört. Oder man hat zu viele Treffer. Solange man in den Flows aber keine zusätzlichen technischen IDs für die einzelnen Elemente einführt, existiert einfach keine bessere Lösung.

Aufpassen muss man auch, wenn man komplette Flows mocken möchte: Dafür muss man tatsächlich den Flow selbst mocken, nicht die aufrufende Flow-Ref. Außerdem generiert das Studio an der Stelle aktuell noch falsche Werte im Wizard: Flows haben kein doc:name, sondern nur ein name. Bei Sub-Flows ist noch zu beachten, dass man mit matchContains(...) arbeitet (siehe MUnit-Dokumentation).

Was machen wir im Mock? Er soll den SOAP-Aufruf ersetzen, also laden wir hier eine Payload mit dem SOAP-Ergebnis aus einer Datei und stellen sie als Stream zur Verfügung. Zusätzlich ließen sich hier auch – wie am Anfang des Tests – Properties definieren.

Anschließend folgt die Prüfung des Ergebnisses. Ausnahmsweise habe ich hier mal nicht zu meinem Lieblingswerkzeug DataWeave gegriffen, sondern zu den guten alten Mule-Hausmitteln: Das JSON wird zu einer Map gewandelt, aus der man anschließend mittels MEL die Werte holt und vergleicht:

  <json:json-to-object-transformer returnClass="java.util.Map" doc:name="JSON to Object"/>
  <munit:assert-on-equals message="BMI wrong" expectedValue="25" actualValue="#[java.lang.Math.round(payload.bmi) + '']" doc:name="Assert BMI"/>
  <munit:assert-on-equals message="Beschreibung wrong" expectedValue="Normalgewicht" actualValue="#[payload.beschreibung]" doc:name="Assert Beschreibung"/>

Hier gilt noch die alte Entwicklerweisheit: Fließkommawerte nie auf Gleichheit testen (passt nie), sondern immer runden.

Spy

Auf das Vorspiegeln falscher Tatsachen (Mock) folgt jetzt die Spionage (Spy): Wir schauen dem Flow über die Schulter bzw. direkt ins Herz: Ein Spy wird wie ein Mock auf einen Message-Prozessor losgelassen. Statt jedoch den originalen Aufruf zu ersetzen, lassen sich darin Werte überprüfen. Ein Spy hat keine Lizenz zum Töten, noch schlimmer: Er darf die Payload nicht verändern! Das erzeugt gewisse Probleme: Eigentlich wollte ich die Werte vor dem SOAP-Aufruf überprüfen. Die liegen aber als Stream vor. Lese ich ihn ein, ist er weg (bzw. leer). Eigentlich egal, schließlich haben wir den SOAP-Call ja gegen einen Mock ausgetauscht. Aber Mule lässt uns hier die Payload nicht verändern. Was bleibt? Wir speichern den Stream – der ja nur eine Referenz auf ein Java-Objekt ist – in einer Flow-Variablen. Wir können den Inhalt dann später prüfen. Grafisch sieht das folgendermaßen aus:

Kompletter Test-Flow

Oder der Spy im XML:

<mock:spy messageProcessor=".*:.*" doc:name="Spy">
  <mock:with-attributes>
    <mock:with-attribute name="doc:name" whereValue="#['call bmi-soap']"/>
  </mock:with-attributes>
  <mock:assertions-before-call>
    <set-variable variableName="beforeSoap" value="#[payload]" doc:name="Set beforeSoap"/>
  </mock:assertions-before-call>
</mock:spy>

Der Spy wird – wie der Mock – über einen regulären Ausdruck und Attribute auf den Message-Prozessor angesetzt. Anschließend folgen die Spionageoperationen, die vor oder nach dem Message-Prozessor ausgeführt werden. Hier ist nur „vorher“ sinnvoll, sonst würden wir nur unseren Mock testen. Statt der Prüfung landet der Stream in der Flow-Variablen beforeSoap.

Die Prüfung folgt anschließend im Flow. Mittels DataWeave holen wir uns den interessanten Teil aus der SOAP-Antwort:

  <dw:transform-message metadata:id="1b10de07-fcc6-49ff-ae10-7042e019d03a" doc:name="Extract input">
    <dw:input-variable variableName="beforeSoap" doc:sample="input.xml" mimeType="application/xml"/>
    <dw:set-payload resource="classpath:extract-input.dwl"/>
  </dw:transform-message>

Das zugehörige DWL-Script:

%dw 1.0
%output application/java
%namespace bmi http://codecentric.de/services/bmi
---
flowVars.beforeSoap.bmi#calculateBmi.input

Und die Prüfung:

<munit:assert-on-equals message="length wrong" expectedValue="1.8" actualValue="#[payload.length]" doc:name="Assert length"/>
<munit:assert-on-equals message="mass wrong" expectedValue="80" actualValue="#[payload.mass]" doc:name="Assert mass"/>

Zusammenfassung

Wer bis hier durchgehalten hat und auch schon JUnit kennt, wird die Gemeinsamkeiten erkannt haben: Man schafft Vorbedingungen, führt den Code unter Test aus und nutzt Mocking (nicht Teil von JUnit), um im Rahmen des Tests nicht interessanten Code bzw. hier komplette Systeme zu ersetzen. Während die Mocks die Ausgabewerte der angeschlossenen Systeme liefern, kann man mithilfe der Spys die Eingabewerte validieren. Der Aufwand für den Test liegt schnell im gleichen Bereich wie der eigentliche Flow. Aber wer schon einmal Java-Code nach TDD entwickelt hat, den dürfte auch dies nicht überraschen.

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 markiert *