Mule bietet mit MUnit ein Framework, mit dem sehr ähnlich zu den normalen Flows Tests geschrieben werden können. Ob es sich dabei um Unit- oder Integrationstests handelt, hängt von der Implementierung und der Benennung ab. Denn mithilfe von Maven lassen sich Tests abhängig vom Namen filtern.
Das Vorgehen in Munit-Tests ist grundsätzlich vergleichbar mit „konventioneller“ Softwareentwicklung und die Prinzipien sind dieselben. Tests sind so strukturiert, dass es
- ein Setup gibt (pro Suite oder pro Test),
- die Message für den Testaufruf vorbereitet wird,
- der Testflow aufgerufen wird (flow under Test)
- und anschließend das Ergebnis verglichen wird (assert).
Viele Flows in Mule benötigen externe Services (z.B. Datenbanken, Messaging-Systeme), um Daten abzurufen oder zu aktualisieren. In diesem Artikel beschränken wir uns zunächst auf Datenbanken, konkret auf die relationalen. Insbesondere stelle ich verschiedene Möglichkeiten vor, Datenbanken in Mule zu mocken.
Der vollständige Code für die gesamte Artikelserie liegt in gitlab .
Scenario-Beschreibung
Die Anwendung, die als Beispiel dienen soll, ist ziemlich einfach. Sie bietet einen REST-Service an, bei dem User abgefragt werden können. Der entsprechende Ausschnitt der Raml-Definition erlaubt das Abfragen einer Liste von allen oder eines einzelnen Users:
1/users/{id}: 2 get: 3 responses: 4 200: 5 body: 6 application/json: 7 example: | 8 { 9 "id": "1234", 10 "name": "Doe", 11 "firstname": "John", 12 "user": "jdoe" 13 } 14 404:
Ich nehme an, dass in der Produktion eine Postgres-DB eingesetzt wird, sagen wir in der Version 10.6, aber zunächst spielt das eine untergeordnete Rolle. In der Datenbank gibt es ein einfaches Schema „mule“ mit einer Tabelle „users“:
1CREATE TABLE public.users( 2 username character varying NOT NULL, 3 lastname character varying NOT NULL, 4 firstname character varying, 5 id bigint NOT NULL DEFAULT nextval('users_id_seq'::regclass) 6);
Die eigentliche Implementierung (ich überspringe den Teil, in dem mit dem APIKit der Rumpf der Anwendung gebaut wird) ist ebenfalls sehr einfach: Um einen bestimmten User zu finden, wird eine entsprechende SQL-Query verwendet – mit der id des Users als Parameter. Anschließend wird mit Validate geprüft, ob auch ein User gefunden wurde und wenn ja, der User nach JSON konvertiert. Falls der User nicht gefunden wird (der „Is not empty“ Validator schlägt an) wird eine org.mule.module.apikit.exception.NotFoundException
geworfen, die vom APIKit-Rumpf gefangen und in einen HTTP 404-Status umgewandelt wird:
Soweit, so gut. Diese Anwendung soll nun getestet werden. Der Einfachheit halber werden wir hier nur diesen Flow testen, nicht die drumherum aufrufenden APIKit-Flows (ich beschränke mich also auf die Teile, die ich üblicherweise mit Unit-Tests abdecke).
Die Hauptfrage, die in diesem Artikel gestellt wird, ist: Wie gehe ich mit dem Datenbank-Zugriff um? Es gibt mehrere Ansätze dafür:
- Mocken der Datenbank,
- Benutzen einer Mock-Datenbank (häufig eine In-Memory-H2-Datenbank)
- Tests laufen gegen eine echte Datenbank, die mit den Tests gestartet wird (Container)
- Testen gegen die Produktions-Datenbank (ok, Scherz)
Testen gegen eine produktionsähnliche Datenbank, die in einer dedizierten Testinfrastruktur bereitgestellt wird.
Im Prinzip entsprechen diese Optionen den üblichen Arten von Tests:
- Unit-Tests
- Unit-/Modultests
- Integrationstests
- End-2-End und Acceptancetests
Natürlich ist der beschriebene Anwendungsfall trivial. Das erlaubt mir aber, diese Optionen auch anhand eines übersichtlichen Flows zu demonstrieren, so dass ich keine Unterscheidung zwischen der Testarten mache. In realen Umgebungen, wenn die Flows komplexere Logik enthalten oder die Aufrufhierarchie komplexer wird, werden die Testtypen dann auch wegen unterschiedlichen Intentionen und Laufzeiten unterschieden und implementiert.
Unit-Tests mit gemockten Datenbanken
Überlegen wir zunächst, welche Funktionen eigentich getestet werden müssen:
- Es gibt den „Happy-Path“, der den User findet und als JSON in der Payload zurück liefert.
- Dann gibt es einen Ablauf, in dem kein User gefunden wird (wir erwarten eine entsprechende Exception).
- Und dann kann es passieren, dass aus der Datenbank eine Exception geworfen wird, weil die Verbindung abbricht, keine Berechtigung vorhanden oder das SQL-Statement fehlerhaft ist.
Den technischen Fehlerfall ignorieren wir an dieser Stelle – er ist, vergleichbar zum zweiten Fall, mit einer erwarteten Exception zu implementieren.
Die Unit-Tests in Mule werden mit MUnit implementiert. Dazu haben Kollegen bereits einige gute Artikel geschrieben (z.B. die Artikelserie Mule-Anwendungen mit MUnit testen ).
In der Übersicht sehen die Testfälle wie in den folgenden Bildern aus. Grundsätzlich ist die Struktur ähnlich wie bei Unit-Tests in herkömmlichen Sprachen: Ein Setup (ggf. ein Before-Suite/Before-Test) dient der Definition der Mocks (und Spies), dann werden die Eingangsdaten definiert, der Flow-under-Test aufgerufen und anschliessend das Ergebnis geprüft:
In einem ersten Schritt betrachten wir das Mocking der Datenbank, d.h. wir kappen die Verbindung zum Konnektor und tun nur so, als ob eine Datenbank vorhanden wäre:
1<mock:when ... messageProcessor=".*:.*"> 2 <mock:with-attributes> 3 <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/> 4 </mock:with-attributes> 5 <mock:then-return ...> 6</mock:when>
Dies ist die Struktur der Mocking-Definition, die im Setup des Tests verwendet wird. Die Kombination aus mock:when mit dem Ausdruck im messageProcessor und den Attributen in with-attribute bestimmt den Prozess-Schritt, der gemockt werden soll. In diesem Beispiel wird lediglich der Name des Schritts zur Identifikation verwendet. In den allermeisten Fällen ist das ausreichend, da die Schritte (gerade bei kurzen Flows) eindeutig sein sollten. Ist das nicht der Fall, können weitere Bedingungen oder eine genauere Bestimmung des Message-Prozessors verwendet werden (für DB-Selects z.B. „db:select“). Das wäre auch hier eine Alternative zur Selektion mit dem Attribut, da es nur einen DB-Select in diesem Flow gibt. Ein Vorteil wäre sogar, dass der Test auch ohne Anpassung funktioniert, wenn der Schritt „Lookup user by id“ im Flow-under-Test umbenannt würde.
Der Happy-Path
Grundsätzlich werden beim Mocking die Endpoints abgeklemmt und definiert, wie diese beim Aufruf zu reagieren haben. Im Happy-Path soll die Datenbank in diesem Beispiel einen gültigen Record zurück geben. Das geschieht unter Verwendung von mock:then-return:
1<mock:when doc:name="DB returns 1 record" messageProcessor=".*:.*"> 2 <mock:with-attributes> 3 <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/> 4 </mock:with-attributes> 5 <mock:then-return payload="#[['id': 1234, 'firstname': 'John', 'lastname': 'Doe', 'username': 'jdoe']]]" 6 mimeType="application/java"/> 7</mock:when>
Zum Prüfen der Ergebnisse gibt es unterschiedliche Alternativen: Man könnte einzelne Felder extrahieren und mit assert prüfen (viel Arbeit), die Ergebnisse 1:1 vergleichen (dann führen aber eigentlich irrelavante Unterschiede wie Formatierungen in XML oder JSON zu Fehlern), die Konvertierung in ein einfach vergleichbares Format (z.B. dedizierte Java-Klassen) oder die Verwendung einer Vergleichsfunktion. Ich nutze gerne das Modul assert-Object-equals , mit dem sehr einfach auch JSON oder XML-Ojekte verglichen werden können:
1<assert-object-equals:compare-objects 2 expected-ref="#[getResource('user-expected.json').asStream()]" 3 doc:name="user is returned" />
Das File „user-expected.json“ hat das erwartete User-Objekt im Json-Format zum Inhalt und liegt zusammen mit anderen Testdaten im Verzeichnis src/test/resources, welches Bestandteil des classpath ist:
1{ 2 "id": 1234, 3 "user": "jdoe", 4 "name": "Doe", 5 "firstname": "John" 6}
User not found
Natürlich muss man den Fall prüfen, dass der User nicht gefunden wird. In diesem Fall liefert der Database-Connector eine leere Liste:
1<mock:when doc:name="DB returns empty list" messageProcessor=".*:.*"> 2 <mock:with-attributes> 3 <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/> 4 </mock:with-attributes> 5 <mock:then-return payload="#[[]]" mimeType="application/java"/> 6</mock:when>
Im Test erwarten wir jetzt aber kein Ergebnis, das wir vergleichen können, sondern eine Exception. Dieses Verhalten prüfen wir mit der passenden Deklaration des Tests:
1<munit:test name="get_users_idTest-user-not-found" 2 description="When user is not found a NotFoundException is expected" 3 expectException="org.mule.module.apikit.exception.NotFoundException">
Damit stellen wir sicher, dass der Flow-under-Test mit der angegebenen Exception abbricht – alles andere läßt den Test fehlschlagen.
Zusammenfassung
In diesem ersten Artikel dieser Serie habe ich das Mocking von Datenbanken in MUnit-Tests eingeführt. Dabei habe ich anhand eines sehr einfachen Beispiels die Struktur von MUnit-Tests beschrieben und Tests für verschiedene Ergebnisse von Datenbankaufrufen geschrieben.
In nächsten Artikel werde ich beschreiben, wie Tests geschrieben werden können, die auch das aufrufende API-Kit mit testen und wie anstelle des Abklemmens der Datenbank eine immer noch schnelle, für viele Tests geeignete In-Memory-Datenbank verwendet werden kann.
Weitere Beiträge
von Christian Langmann
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
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
Christian Langmann
Solution 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.