Testen in Mule mit Datenbanken – Teil 1: Mocking von Datenbanken

Keine Kommentare

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

  1. ein Setup gibt (pro Suite oder pro Test),
  2. die Message für den Testaufruf vorbereitet wird,
  3. der Testflow aufgerufen wird (flow under Test)
  4. 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:

/users/{id}:
  get:
    responses:
      200:
        body:
          application/json:
            example: |
            {
               "id": "1234",
               "name": "Doe",
               "firstname": "John",
               "user": "jdoe"
            }
      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“:

CREATE TABLE public.users(
    username character varying NOT NULL,
    lastname character varying NOT NULL,
    firstname character varying,
    id bigint NOT NULL DEFAULT nextval('users_id_seq'::regclass)
);

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:

Flow get_users_id

Flow für GET /users/{id}

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:

  1. Mocken der Datenbank,
  2. Benutzen einer Mock-Datenbank (häufig eine In-Memory-H2-Datenbank)
  3. Tests laufen gegen eine echte Datenbank, die mit den Tests gestartet wird (Container)
  4. 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:

  1. Unit-Tests
  2. Unit-/Modultests
  3. Integrationstests
  4. 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:

test-flows

Test-Flowsfür user-found und user-not-found

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:

<mock:when ... messageProcessor=".*:.*">
    <mock:with-attributes>
        <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/>
    </mock:with-attributes>
    <mock:then-return ...>
</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:

<mock:when doc:name="DB returns 1 record" messageProcessor=".*:.*">
    <mock:with-attributes>
        <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/>
    </mock:with-attributes>
    <mock:then-return payload="#[['id': 1234, 'firstname': 'John', 'lastname': 'Doe', 'username': 'jdoe']]]"
        mimeType="application/java"/>
</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:

<assert-object-equals:compare-objects
    expected-ref="#[getResource('user-expected.json').asStream()]"
    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:

{
    "id": 1234,
    "user": "jdoe",
    "name": "Doe",
    "firstname": "John"
}

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:

<mock:when doc:name="DB returns empty list" messageProcessor=".*:.*">
    <mock:with-attributes>
        <mock:with-attribute name="doc:name" whereValue="#['Lookup user by id']"/>
    </mock:with-attributes>
    <mock:then-return payload="#[[]]" mimeType="application/java"/>
</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:

<munit:test name="get_users_idTest-user-not-found"
    description="When user is not found a NotFoundException is expected"
    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.

Christian Langmann

Christian ist seit 2013 für codecentric als Solution Consultant unterwegs. Er begeistert sich für Craftsmanship im gesamten Lebenszyklus der Softwareentwicklung, insbesondere für Continuous Delivery und Software Architekturen. Das Zusammenbringen verschiedener Skills im Team ist für ihn eine der interessantesten Herausforderungen. Daneben sind Integrationstechnologien sein Steckenpferd.

Kommentieren

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