Im ersten Teil dieser Artikelserie hatte ich versprochen, einen SOAP-Service mit MUnit zu testen, also muss ich das wohl heute einlösen. SOAP ist dabei der Vorwand, die Versorgung mit Testdaten und die Überprüfung der Ergebnisse (assert) vorzuführen.
Was brauchen wir zum Testen eines SOAP-Services? Ein Mule-Projekt mit einer SOAP-Schnittstelle. Mule kann SOAP in zwei grundsätzlichen Varianten anbieten:
- Als Proxy/Adapter für ein existierendes Backend, in dem die Fachlogik lebt.
- Mule selbst bietet den Service an.
Die erste Variante passt besser in das ESB-Umfeld, schließlich soll der ESB keine Fachlichkeit selbst implementieren. Würde ich das heute nutzen, müsste ich jedoch den „echten“ Service mocken, was ich mir aber für den nächsten Teil der Serie vorgenommen habe.
Also bleibt – zumindest für diesen Blogpost – nur die zweite Variante: Mule selbst hostet den Service. Dank CXF innerhalb von Mule funktioniert das ohne Klimmzüge, hier der zugehörige Flow:
Links beginnen wir mit einem synchronen http-Endpoint, der alle Nachrichten direkt in eine CXF-Komponente leitet. Die CXF-Server-Komponente trennt die XML-Welt auf der linken Seite von der Java-Welt auf der rechten Seite: Rechts stecken in der Nachricht die Aufrufargumente für die Java-Methode, die von der Java-Komponente implementiert wird. Das Java-Objekt mit dem Ergebnis wandelt die CXF-Komponente wieder in eine SOAP-XML-Nachricht um, die der http-Endpoint auf der linken Seite an den Aufrufer zurückgibt.
Wie man an dem Beispiel sieht, benötigt man in Mule neben den annotierten Klassen (Interface, Implementierung, Aufruf- und Ergebnisobjekte) nur noch einen kleinen Flow mit drei Elementen – fertig ist der CXF-Service. Hängt man an die Service-URL noch ein „?wsdl“, so liefert Mule auch die passende WSDL-Datei des Service aus. (Wer lieber WSDL-Dateien statt annotierter Klassen schreibt, kann natürlich auch aus der WSDL die annotierten Klassen generieren lassen.)
Vorbereitung
Nun aber zum Test: Bevor ich automatisiere, teste ich den Service mit SoapUI manuell. Den Testrahmen erzeugt SoapUI automatisch, es braucht dabei nur die WSDL (die Mule praktischerweise liefert). SoapUI generiert auf Basis der WSDL das Gerüst der Requests. In diese trägt man nur noch die Testdaten ein, hier ein Beispiel:
<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>1.88</length>
<mass>77</mass>
</input>
</bmi:calculateBmi>
</soapenv:Body>
</soapenv:Envelope>
Das zugehörige Projekt stammt noch aus der Weihnachtszeit, da bot sich als Beispiel ein Service an, der auf Basis von Größe und Gewicht den so genannten „Body Mass Index“ (kurz: BMI) berechnet. Ruft man den Service in SoapUI auf (und hat in Mule keinen Fehler gemacht), zeigt SoapUI das Ergebnis an:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:calculateBmiResponse xmlns:ns2="http://codecentric.de/services/bmi">
<return>
<bmi>21.79</bmi>
<description>Normalgewicht</description>
</return>
</ns2:calculateBmiResponse>
</soap:Body>
</soap:Envelope>
(Ich könnte den Service mal erweitern, so dass er einem noch direkt ausrechnet, wie viele Kilo man bei gegebener Größe noch zunehmen darf, bis man in den Bereich „Übergewicht“ wechselt…)
Warum schreibe ich hier so viel über SoapUI, wo es doch eigentlich um MUnit gehen soll? Einfacher komme ich nicht an die Ein- und Ausgabedaten für den MUnit Test. Den erstellen wir mit einem Wizard, den wir per Klick mit der rechten Maustaste auf den Header des zu testenden Flows erreichen:
Über das Untermenü „Create new suite“ stoßen wir den Wizard an (okay, das hatten wir schon im ersten Teil…). Gegenüber dem ersten Teil können wir den Testfall allerdings nicht direkt erfolgreich ausführen, es fehlen noch die Eingabedaten für den SOAP-Service.
Eingabe
Die Eingabedaten (aus SoapUI kopiert) speichern wir als XML-Datei in dem Ordner src/test/resources. Ich ergänze noch ein Package, hier passend zur Implementierung de.codecentric.bmi. Wie kommen die Daten in den Test-Flow? Für kurze Strings bietet sich set-payload aus der Standard-Mule-Palette an, eine längere XML-Datei passt aber nicht sinnvoll in das Eingabefeld (und sieht dann wegen des Quotings aller Sonderzeichen in der XML-Ansicht des Flows gruselig aus). Daher habe ich die XML-Daten einer Datei gespeichert. Deren Inhalt müssen wir jetzt in die Payload bekommen.
Mule bietet dazu in der Palette im Bereich MUnit das Element „Set Message“ (munit:set im XML) an. Auf den ersten Blick sieht man dort auch nur ein kleines Einabefeld (wie bei „Set Payload“). Allerdings gibt es hier in der MEL noch einige zusätzliche Funktionen, eine davon zum Laden von Ressourcen aus dem Classpath:
#[getResource(‚de/codecentric/bmi/sample-input.xml‘).asString()]
Die Methode getResource() erzeugt eine Instanz der Klasse MunitResource mit folgenden Methoden:
- asStream()
- asString() (Encoding ist leider nicht einstellbar, es ist immer das Standardenconding des Betriebssystems)
- asByteArray()
Für die XML-Payload besteht die richte Wahl aus asString().
Die GUI des AnypointStudio sperrt sich – in der aktuellen Version – dagegen, das Element „Set Message“ links neben der Flow-Reference abzulegen. Ein Workaround ist einfach: zuerst rechts neben die Flow-Reference legen, anschließend die Flow-Reference nach rechts ziehen, schon stimmt die Reihenfolge wieder.
Somit hätten wir die Daten für den SOAP-Aufruf geladen, der Testfall sollte damit also funktionieren. Probieren wir es also aus: Ja, bei mir ist der Balken grün.
Dummerweise bleibt der Balken auch dann grün, wenn das Ergebnis falsch ist. Sogar eine Exception in der Implementierung lässt den Balken nicht rot werden, da SOAP im Fehlerfall nur ein XML mit der Exception als Inhalt liefert. Es fehlt noch die Verifikation des Ergebnisses.
Verifikation
Die Verifikation der Ergebnisse funktioniert ähnlich wie in JUnit-Tests, mit „Assert Payload“ statt assertEquals(). Dazu kopieren wir uns vorher die Ausgabe aus SoapUI in eine Datei sample-output.xml und laden die Datei über getResource() (siehe oben).
Laufen lassen, roter Balken…
Ein genauer Blick in den Errors-Tab der MUnit-View oder ein Logger zeigt, dass wir kein XML zurückbekommen, sondern folgendes Objekt:
org.mule.module.cxf.CxfInboundMessageProcessor$2@5cc1bf20
Wer schon länger mit Mule arbeitet, sieht hier wieder Streaming am Werk und kennt den Standardtrick: Ein Object-to-String-Transformer zwischen Flow-Reference und dem Assert materialisiert den Stream in einen String.
Laufen lassen, immer noch ein roter Balken…
Schaut man sich wieder den Error-Tab an, erkennt man das Problem: Das XML aus SoapUI ist hübsch formatiert, aus CXF kommt jedoch ein kompaktes XML-Dokument in einer einzigen Zeile. Wir bekommen den Test grün, wenn wir die Ergebnisdatei ebenso in eine einzige Zeile schreiben.
Laufen lassen, grüner Balken…
Die Lösung funktioniert zwar, taugt aber ansonsten eher als schlechtes Beispiel:
- Der Testfall schlägt fehl, sobald sich die Formatierung des XML ändert, auch wenn die Daten korrekt sind.
- Das XML in einer langen Zeile ist komplett unleserlich. Wartbarkeit sieht anders aus.
Konzentrieren wir uns daher auf die wichtigen Dinge: Die Struktur und der Inhalt des XML müssen stimmen.
Die Struktur einer XML-Datei finden wir (hoffentlich) in einer XSD. Falls sie nicht als separate Datei vorliegt und in die WSDL inkludiert wurde, ist es jetzt Zeit, sie zu extrahieren und in eine eigene Datei zu legen. Mule validiert XML mit Hilfe des schema-validation-filter gegen eine XSD. Letztere lässt sich aus dem Classpath laden, kann also unter src/main/resources oder src/test/resources liegen, je nachdem, ob wir sie nur für den Test oder auch im Produktionscode benötigen.
Das XSD-Schema beschreibt nur das Ergebnisobjekt, in der Nachricht steckt jedoch eine komplette SOAP-Response, inklusive Envelope- und Body-Tag. Mit Hilfe von DataWeave entfernt man die beiden:
%dw 1.0
%output application/xml
%namespace soap http://schemas.xmlsoap.org/soap/envelope/
---
payload.soap#Envelope.soap#Body
Jetzt haben wir ein XML in der Payload stehen, das zur XSD passt.
Eine Falle lauert noch beim Filter: Schlägt die Validierung fehl, bricht Mule den Flow an dem Filter kommentarlos ab. Die Asserts – die wir gleich noch einbauen – führt Mule nicht mehr aus, und MUnit bekommt nicht mehr die Möglichkeit, einen Fehler anzuzeigen. Möchte man beim Fehlschlagen des Filters eine Exception werfen, schachtelt man den Validation-Filter in einen Message-Filter, bei dem throwOnUnaccepted auf „true“ gesetzt wird. So sieht der zugehörige Flow-Schnipsel im XML aus:
<message-filter throwOnUnaccepted="true" doc:name="Check structure">
<mulexml:schema-validation-filter
name="check-response"
schemaLocations="de/codecentric/bmi/bmi-response.xsd"
returnResult="true"
doc:name="Schema Validation"/>
</message-filter>
Jetzt steht hinter dem Filter ein XML mit garantiert korrekter Struktur in der Payload. Daraus können wir mittels DataWeave zwei Flow-Variablen extrahieren: bmi und description. Da der Filter den Mime-Type nicht auf application-xml setzt, müssen wir in DataWeave den Mime-Type explizit angeben (funktioniert leider nur im XML und nicht über den GUI-Editor):
<dw:input-payload mimeType="application/xml" />
Das folgende DataWeave-Script extrahiert aus dem XML den bmi:
%dw 1.0
%output application/java
%namespace bmi http://codecentric.de/services/bmi
---
payload.bmi#calculateBmiResponse.return.bmi
Das Script für die description sieht ähnlich aus (das überlasse ich dem Leser als Übung).
Jetzt steht alles bereit für die zwei finalen Asserts:
<munit:assert-on-equals message="bmi"
expectedValue="22.35" actualValue="#[bmi]" doc:name="Assert bmi" />
<munit:assert-on-equals message="description"
expectedValue="Normalgewicht" actualValue="#[description]"
Und zu guter Letzt auch noch eine grafische Darstellung des vollständigen Test-Flows:
Fazit
Hat es sich gelohnt? Von mir kann ja nur ein „Ja“ kommen, sonst hätte ich mir den Blog-Beitrag ja sparen können. Also muss ich wohl eine Begründung liefern: Auf der Haben-Seite steht eine übersichtliche Darstellung in der GUI, die den Ablauf des Tests klar visualisiert:
- Daten laden
- Aufruf des Testflows
- Body extrahieren (SOAP-Envelope mit DataWeave abstreifen)
- Schema-Validierung
- Extrahieren der Werte (ebenfalls DataWeave)
- Ein Assert für jeden der beiden Werte
Schlägt irgendetwas fehl, sieht man in der Fehlermeldung auch direkt die Ursache: Struktur, bmi, description.
Und wie sieht es im XML aus? Zwölf Zeilen für Namespaces etc. plus 32 Zeilen für den eigentlichen Testfall. Zugegeben, in der Länge könnte Java das mit dem passenden SOAP-Framework eventuell unterbieten. Dann darf man aber auf keinen Fall die (generierten) Zeilen für das Interface und die Transportobjekte mitzählen.
Das ist natürlich erstmal ein einfaches Beispiel. In weiteren Folgen werde ich noch auf das Thema Mock eingehen. Vorher schiebe ich eventuell aber noch etwas zum Thema tabellenbasiertes Testen ein, mal sehen…
Links
- Transformieren von Nachrichten mit Mule DataWeave – Teil 1: Einführung https://blog.codecentric.de/2015/09/esb-serie-transformieren-von-nachrichten-mit-mule-dataweave-teil-1-einfuehrung/
- Mule Anwendungen mit MUnit Testen (Teil 1): Start im Anypoint Studio: https://blog.codecentric.de/2016/04/mule-munit-anypoint-studio/
- Mule-Anwendungen mit MUnit testen (Teil 3): Tabellenbasierte Tests https://blog.codecentric.de/2016/05/mule-munit-tabellenbasierte-tests/
- Alle Blog-Artikel zu Mule im codecentric-Blog: https://blog.codecentric.de/?s=mule
- Was ist ein ESB und wofür kann man ihn nutzen? https://blog.codecentric.de/2012/11/was-und-wofur-esb/
- Tutorial „Enterprise Service Bus mit Mule ESB“: Hello World/Bus! https://blog.codecentric.de/2012/12/enterprise-service-bus-mule-esb/
Weitere Beiträge
von Roger Butenuth
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Roger Butenuth
Senior Integration Architect
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.