Testen von Mule ESB Applikationen (Teil 2/3): Integrationstests und (Endpoint) Mocking mit MUnit

Keine Kommentare

Abstrakt

Im allgemeinen Konsens wird das Testen von Software als integraler Bestandteil des Software Entwicklungsprozesses gesehen. Tests sollten in allen Phasen der Softwareentwicklung eingesetzt werden: von Unit bis zu Akzeptanztests. Vor allem im Software Engineering bilden zusammenhängende und automatisierte Tests ein Sicherheitsnetz gegen regressive und inkompatible Änderungen.

In Integrationsprojekten mit Mule ESB sind diese Aspekte auch von Belang. Komponenten in Mule Flows, die Flows selber und deren Integration müssen intensiv getestet werden.

Dieser Artikel ist der zweite Teil einer Serie zum Thema des Testens von Mule ESB Projekten auf allen Ebenen. Der Fokus dieses Artikels liegt auf dem Testen der Integration des gesamten Flows in einem Mule Projekt welcher aus bereits getesteten kleineren Komponenten und Sub Flows besteht.

MUnit

MUnit ist ein Mule Test Framework welches ursprünglich als Nebenprojekt in Mule seinen Anfang nahm und später zu einem Open Source Projekt wurde. Es unterstützt das automatisierte Testen von Mule Applikationen für Mule Entwickler. Es ist in diversen Unternehmen und internen Mule Projekten im Einsatz [1].

Das Projekt ist auf GitHub unter folgender Adresse zu finden: https://github.com/mulesoft/munit

Abhängigkeiten

Um mit MUnit beginnen zu können, benötigt man folgende Abhängigkeiten:

<dependency>
    <groupId>org.mule.munit</groupId>
    <artifactId>munit-common</artifactId>
    <version>${munit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mule.munit</groupId>
    <artifactId>munit-runner</artifactId>
    <version>${munit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mule.munit.utils</groupId>
    <artifactId>munit-mclient-module</artifactId>
    <version>${munit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mule.munit</groupId>
    <artifactId>munit-mock</artifactId>
    <version>${munit.version}</version>
    <scope>test</scope>
</dependency>

Test Fall Definition – XML vs. Java

Wenn wir Integrationstests mit MUnit bauen, benutzen wir auf einer technischen Ebene ihre Test Infrastruktur für Modultests. Das bedeutet das wir JUnit Tests für die Modultests der Komponenten und Sub Flows und für die Integrationstests des gesamten Flows ausführen. Diese können mit MUnit auf zwei Arten gebaut werden. Entweder mit einer XML Beschreibung, welche in dem Kontext auch als „Mule code“ bezeichnet wird, oder mit eine Java Fluent API.

Wir stellen hier das MUnit Hello World Beispiel vor [2] [3]. Wir nehmen an das der folgende Flow getestet werden soll. Dabei handelt es hierbei um einen typischen Sub Flow ohne Inbound Endpoints:

<flow name="echoFlow" doc:name="echoFlow">     
 <echo-component/> 
</flow>

Der passende „Mule code“ der den Flow testen würde, sieht wie folgt aus:

<!-- Load the config of MUnit -->
<munit:config/>
 
<!-- Load the definition of the flow under test -->
<spring:beans>
 <spring:import resource="mule-config.xml"/>
</spring:beans>
 
<!-- Define the test case -->
<munit:test name="testingFlow"
        description="We want to test that the flow always returns the same payload as we had before calling it.">
 
 <!-- Define the input of the test case -->
 <munit:set payload-ref="#[string: Hello world!]"/>
 <!-- Call the flow under test -->
 <flow-ref name="echoFlow"/>
 <!-- Assert the test result -->
 <munit:assert-not-null/>
 <munit:assert-that payloadIs-ref="#[string: Hello world!]"/>
</munit:test>

Für das gleiche Beispiel muss die JUnit Test Klasse, welche mit der Mule Java Fluent API getestet soll, von der FunctionalMunitSuite Klasse erben. Dies würde wie folgt aussehen:

public class FirstTest extends FunctionalMunitSuite {
 
 /**
 * This can be omitted. In that case, the config resources will be taken from mule-deploy.properties file.
 * @return The location of your MULE config file. 
 */
 @Override
 protected String getConfigResources() {
  return "mule-config.xml";
 }
 
 @Test
 public void testEchoFlow() throws Exception {
  // Start the flow "echoFlow" with Mule event from testEvent(...) with the payload "Hello world!"
  MuleEvent resultEvent = runFlow("echoFlow", testEvent("Hello world!"));
 
  // Get the payload result from the flow and assert the result
  assertEquals("Hello world!", resultEvent.getMessage().getPayloadAsString());
 }
}

Die protected String getConfigResources() Methode erwartet eine Komma separierte Liste von Mule und Spring XML Dateien und stellt diese dem Unit Test zur Verfügung. Dabei sollten Flow Beschreibungen und Konfigurationen für Produktion und Test entkoppelt vorliegen.

Wenn es um den Vergleich beider Ansätze geht, kann man argumentieren, dass es sich nur um eine Geschmacksache handelt. Dies gilt für einfach Flows und Test Fälle. Vor allem, wenn man Verifikation und Spying aus MUnit (wird weiter unten beschrieben) verwendet, can man argumentieren das XML besser zu lesen sei. Doch, wenn man viele Test Fälle und viele Sub Flows hat, ist die Wiederverwendung von Test Code eine Notwendigkeit. Aus diesem Grund, bevorzugen wir den Java Ansatz über den XML Ansatz, da er uns eine leichtere Wiederverwendbarkeit von Hilfsklassen, Test Konfigurationen und Oberklassen erlaubt. Aus diesem Grund, bleiben wir ab jetzt bei Java Beispielen, weisen aber darauf hin, das es jeweils immer eine XML Alternative dazu gibt.

Starten eines synchronen Flows

Wie im letzten Beispiel dargestellt, wird dieser einfach Flow durch die Verwendung der protected final MuleEvent runFlow(String name, MuleEvent event) throws MuleException Methode aus der FunctionalMunitSuite Klasse gestartet. Dies ist auch möglich, wenn ein Flow einen Inbound Endpoint besitzt, der normalerweise in Produktion diesen Flow starten würde.

Wir nehmen an, das wir einen Preis Ermittlungsdienst haben, der für einen Großhändler die Preise für ein bestimmtes Produkt bei drei Zulieferer erfragt und dann die aggregierten drei Preise zurück gibt. Diese Integration der drei Zulieferer, könnte wie folgt aus sehen:

<jms:inbound-endpoint exchange-pattern="request-response" queue="QuoteQueue" doc:name="JMS"/>
<scatter-gather doc:name="Scatter-Gather">
  <!-- JMS call -->
 <jms:outbound-endpoint exchange-pattern="request-response" queue="Supplier1Queue" doc:name="JMS"/>
 <!-- SOAP call -->
 <processor-chain>
  <cxf:jaxws-client serviceClass="de.codecentric.example.PricingInterface" operation="getPrice" doc:name="CXF"/>
  <http:outbound-endpoint exchange-pattern="request-response" host="localhost" port="7000" path="supplier2" doc:name="HTTP"/>
  <object-to-string-transformer doc:name="Object to String"/>
 </processor-chain>
 <!-- REST call -->
 <processor-chain>
  <http:outbound-endpoint exchange-pattern="request-response" host="localhost" port="9000" path="supplier3/getPrice/#[payload]" method="GET" doc:name="HTTP"/>
  <object-to-string-transformer doc:name="Object to String"/>
 </processor-chain>
</scatter-gather>
<notification:send-business-notification config-ref="NotificationConfig" message="Gathered prices #[payload]" uuid="#[flowVars.uuid]" doc:name="Business Notification"/>

Um diesen Flow zu starten muss die protected final MuleEvent runFlow(String name, MuleEvent event) throws MuleException Methode mit einer MuleEvent Instanz versorgt werden. Diese kann entweder über die Hilfsfunktion protected final MuleEvent testEvent(Object payload) throws Exception erzeugt werden oder durch direkte Instanziierung. Der letzte Fall ermöglicht mehr Kontrolle über den Test Fall, da dadurch die Mule Message explizit angepasst werden kann z.B. durch das Setzen von Mule Properties:

@Test
public void testPricingFlow() {
  // Create MuleMessage with a String payload
  MuleMessage mockedInboundMsg = muleMessageWithPayload("PROD123");
  // Additional properties for the message can be set
  mockedInboundMsg.setInvocationProperty("aProperty", "aValue");
  // Create a MuleEvent
  MuleEvent mockedEvent = new DefaultMuleEvent(mockedInboundMsg, MessageExchangePattern.REQUEST_RESPONSE, MuleTestUtils.getTestFlow(muleContext));
  // Run the flow and receive the result of the flow
  MuleEvent flowResult = runFlow("testFlow", mockedEvent);
  ...

Mocking

Wenn es darum geht, alle getestet Komponenten und Sub Flows zusammenzufügen, wird die Aufgabe des Testens der Integration komplizierter. Vor allem, wenn externe Systeme, die nicht während eines Tests verfügbar sind, in diesen Flows verwendet werden. Für Integrationstests, ist das wichtigste Feature des MUnit Frameworks daher die Fähigkeit alle Prozessoren in einem Mule Flow zu mocken [4]. Dies erlaubt einen tiefgehenden Test des gesamten Flows.

Um diesen Flow und die darin enthaltenen Transformationen, Routing und andere Logik zu testen, können der Inbound Endpoint und die drei Outbound Aufrufe auf folgende Weise gemockt werden, bevor der Test ausgeführt und sein Ergebnis überprüft wird:

@Test
public void testPricingFlow() {
 ...
 // Mock the inbound processors to return a mocked input for the flow
 whenMessageProcessor("inbound-endpoint")
        .ofNamespace("http")
        .thenReturn(mockedInboundMsg).getMessage());
 
 // Mock the outbound processors to return a mocked result
 whenMessageProcessor("outbound-endpoint")
        .ofNamespace("jms")
        .thenReturn(muleMessageWithPayload("90 EUR").getMessage());
 
 whenMessageProcessor("outbound-endpoint")
        .ofNamespace("http")
        // Filter by message processor attributes of the endpoint
        .withAttributes(attribute("host").withValue("100.55.32.*"))
        .thenReturn(muleMessageWithPayload("100 EUR").getMessage());
 
 whenMessageProcessor("outbound-endpoint")
        .ofNamespace("http")
        // Filter by message processor attributes of the endpoint
        .withAttributes(attribute("host").withValue(Matcher.contains("200.23.100.190")))
        .thenReturn(muleMessageWithPayload("110 EUR").getMessage());
 
 // Mock a flow processors to return a the same event
 whenMessageProcessor("send-business-notification")
        .ofNamespace("notification")
        .thenReturnSameEvent();
 ...
}

Auf diese Art und Weise hat man die volle Kontrolle über den Flow. In diesem Beispiel sieht man gemockte Inbound Messages, welche zurückgegeben werden um den Inbound Endpoint zu simulieren, und die gemockten Outbound Messages, welche zurückgegeben werden um die externen Aufrufe zu simulieren.

Zu diesem Zweck bietet die Klasse FunctionalMunitSuite die whenMessageProcessor(String nameOfMessageProcessor) Methode an, welche eine Instanz der MessageProcessorMocker Klasse zurückgibt um einen spezifischen Prozessor zu mocken. Dieser gemockte Prozessor kann detaillierter definiert werden durch das Verketten von Attributen mit der public MessageProcessorMocker withAttributes(Attribute ... attributes) Methode. Die public Attribute withValue(Object value) Methode kann dafür zusammen mit der Matchers Klasse oder einer Hilfsklasse aus der FunctionalMunitSuite Klasse verwendet werden. Dies ermöglicht einen höheren Grad an Kontrolle über das Mocking. Zusätzlich kann man sogar Exception Handling in Flows testen, indem man die public void thenThrow(Throwable exception) Methode verwendet.

Asserting, Verifikation und Spying

Um tiefgehende Tests des internen Verhaltens eines Flows durchzuführen, kann man Messages mit Asserts überprüfen, Verifikationen von Prozessor Aufrufen durchführen und über Spying sich an Prozessor Aufrufe hängen [5] [6].

Grundsätzlich können Messages in Mule ganz klassisches in Java mit JUnit Asserts überprüft werden. Dies kann verbessert werden, wenn eine Matchers API wie Hammrest oder AssertJ, welche eine Fluent API bereitstellt, verwendet wird. Wir bevorzugen die Verwendung von AssertJ, weil wir eine Präferenz für Fluent API’s haben.

MUnit verfügt über eine sehr gute Möglichkeit das Verhalten eines Flows zu testen. Es verfügt über ein Verifikation Framework, welches es erlaubt Prozessor Aufrufe nach einem Test zu untersuchen. Bei unserem oben genannten Beispiel möchten wir verifizieren, das alle Outbound Endpoints aufgerufen wurden. Dies können wir am Ende des Tests durch das Verwenden der folgenden Verifikationsmethoden, welche auch einen Assert durchführen:

@Test
public void testPricingFlow() {
 ...
 // Verify JMS outbound endpoint was called one time
 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("jms").times(1);
 
 // Verify HTTP outbound endpoint for supplier 1 was called one time
 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("http")
  .withAttributes(attribute("host").withValue("100.55.32.125")).times(1);
 
 // Verify HTTP outbound endpoint for supplier 2 was called one time
 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("http")
  .withAttributes(attribute("host").withValue("200.23.100.190")).times(1);
 ...
}

Und wieder wird durch die Verwendung der FunctionalMunitSuite Klasse, welche zu diesem Zweck die Methode verifyCallOfMessageProcessor(String nameOfMessageProcessor) zu Verfügung stellt, eine Instanz der MunitVerifier erzeugt um die Verifikation zu definieren. Die Attribute withValue(Object value) Methode hingegen erlaubt in Kombination mit der public MunitVerifier withAttributes(Attribute ... attributes) Methode die Verifikation auf die gleiche Art und Weise anzupassen, wie es beim Mocken von Prozessoren der Fall ist.

Als Alternative zum Asserten am Ende eines Tests, kann Spying verwendet werden, um speziellen Code und Asserts auszuführen, während ein Flow verarbeitet wird. Angenommen man möchte verifizieren das der Input und Output an den Outbound Prozessoren ein valides Format hat, kann man folgende Spys in den Flow hängen:

@Test
public void testPricingFlow() {
 ...
 // Spy on the input and output of the processor
 spyMessageProcessor("outbound-endpoint").ofNamespace("jms")
  .before(new BeforeSpy())
  .after(new AfterSpy());
 
 // Spy on the input and output of the processor
 spyMessageProcessor("outbound-endpoint").ofNamespace("http")
  .withAttributes(attribute("host").withValue("100.55.32.125"))
  .before(new BeforeSpy())
  .after(new AfterSpy());
 
 // Spy on the input and output of the processor
 spyMessageProcessor("outbound-endpoint").ofNamespace("http")
  .withAttributes(attribute("host").withValue("200.23.100.190"))
  .before(new BeforeSpy())
  .after(new AfterSpy());
 ...
}
 
private class BeforeSpy implements SpyProcess
{
 @Override
 public void spy(MuleEvent event) throws MuleException
 {
  // Assert that the payload is a product code which is of type String and starts with PROD
  assertThat(event.getMessage().getPayload()).isOfAnyClassIn(String.class);
  assertThat(event.getMessage().getPayloadAsString()).startsWith("PROD");
 }
}
 
private class AfterSpy implements SpyProcess
{
 @Override
 public void spy(MuleEvent event) throws MuleException
 {
  // Assert that the resulting payload is of type String and is a digit
  assertThat(event.getMessage().getPayload()).isOfAnyClassIn(String.class);
  assertThat(event.getMessage().getPayloadAsString()).matches("^\\d+$");
 }
}

Wie beim Mocken und der Verifikation, sehen wir das gleiche Muster zur Definition von Spy Prozessoren. Die FunctionalMunitSuite Klasse bietet die spyMessageProcessor(String name) Methode an, welche eine Instanz der MunitSpy Klasse erzeugt um den Spy Prozessor zu spezifizieren. Wieder kann die Definition mit Attributen angepasst werden. Dafür können mit den before(final SpyProcess... withSpy) und after(final SpyProcess... withSpy) Methoden Instanzen der Kinderklassen der SpyProcess Oberklasse hinzugefügt werden. Diese werden ausgeführt bevor und nachdem eine Message durch den Message Prozessor geleitet wird, während eines Testdurchlaufs.

Starten von asynchronen oder polling Flows

Um asynchronous Flows zu testen ist folgende zusätzliche Abhängigkeit erforderlich:

<dependency>
   <groupId>org.mule.modules</groupId>
   <artifactId>munit-synchronize-module</artifactId>
   <version>3.5-M1</version>
   <scope>test</scope>
</dependency>

Sie stellt eine Synchronizer Klasse zur Verfügung, welche eine Timeout Infrastruktur enthält. Die Methode process(MuleEvent event) throws Exception muss dabei überschrieben und mit einem Aufruf zum asynchronous Flow ausgestattet werden:

@Test
public void testPricingFlow() {
 ...
 Synchronizer synchronizer = new Synchronizer(muleContext, 20000l) {
 
  @Override
  protected MuleEvent process(MuleEvent event) throws Exception {
   runFlow("asyncPricingFlow", event);
   return null;
  }
 };
 
 MuleEvent event = new DefaultMuleEvent(muleMessageWithPayload("PROD123"), MessageExchangePattern.ONE_WAY, MuleTestUtils.getTestFlow(muleContext));
 synchronizer.runAndWait(event);
 ...
}

Um das Verhalten in einem asynchronem Flow zu überprüfen, empfehlen wir die Verwendung der Spying Funktionalität des MUnit Frameworks. Das Einhängen einer Spy Klasse am letzten Prozessor oder an allen anderen logischen Stellen, kann verwendet werden um die Ergebnisse des Flows zu prüfen.

Um polling Flows zu testen, muss zuerst das Polling deaktiviert werden, idealerweise nachdem der Mule Kontext erzeugt wurde:

@Override
protected void muleContextCreated(MuleContext muleContext) {
 MunitPollManager.instance(muleContext).avoidPollLaunch();
}

Danach können die Test Daten erstellt werden, z.B. eine In-Memory-Datenbank welche vom Flow gepolled wird. Daraufhin kann das Polling in der Synchronizer Klasse gestartet werden:

@Override
protected MuleEvent process(MuleEvent event) throws Exception {
 MunitPollManager.instance(muleContext).schedulePoll("polledFlow");
 return event;
}

Und wieder einmal kann die Spy Funktionalität verwendet werden um das Verhalten des Flows zu testen. Um einen Assert durchzuführen, ob ein asynchroner oder polled Flow sich korrekt verhält, kommen klassische Asserts nach der Durchführung zum Tragen. In unserem Beispiel würde die Test Datenbank abgefragt werden, um das Ergebnis des Test zu verifizieren.

Fazit

In diesem Artikel haben wir dargestellt, wie Integrationstests von Multi Modul Mule Applikation durchgeführt werden könne. Durch die Verwendung von MUnit können einfache Test Fälle mit einer XML Beschreibung oder Java Code gebaut werden. Prozessoren wie zum Beispiel Inbound oder Outbound Endpoints können in Tests germockt werden, wodurch ein integrierter Flow tiefgehend getestet werden kann. Durch die Verwendung von Asserts, Verifikation und Spying kann das korrekte Verhalten eine Flows untersucht werden ohne Produktions Code oder Flow Beschreibungen zu ändern. Zusätzlich haben wir aufgezeigt, welche Anpassungen für das Testen von asynchronen und polling Flows notwendig sind.

Serie

Dieser Artikel ist Teil einer Mule ESB Serie zum Thema Testen von Mule Applikationen:

Referenzen

[1] https://github.com/mulesoft/munit/wiki
[2] https://github.com/mulesoft/munit/wiki/First-Munit-test-with-Mule-code
[3] https://github.com/mulesoft/munit/wiki/First-Munit-test-with-JAVA-code
[4] https://github.com/mulesoft/munit/wiki/Mock-endpoints
[5] https://github.com/mulesoft/munit/wiki/Verify-mock-calls
[6] https://github.com/mulesoft/munit/wiki/Spy-message-processors

Conrad ist Business Development Manager bei der codecentric AG. Er sieht sich selbst als „Coding-Software-Architekt“, Entwickler und in allen anderen Rollen, die für die erfolgreiche Durchführung eines Projekts vonnöten sind. Derzeit fokussiert er sich auf dem Aufbau der Partnerschaft mit Amazon Web Services und auf die in dem Rahmen entstehenden Projekte. Sein persönliches Ziel ist es, nach neuem Wissen in der IT-Industrie zu streben.

In seiner Freizeit macht er sich in Berlin für die IT Community stark, als Gründer und Hauptorganisator des Microservices Meetups Berlin, Co-Organizer des DDD Meetups, des Serverless Meetups und als Mitglied im Organisations-Komitee der MicroXchg Konferenz und KanDDDinsky der Berliner DDD Konferenz.

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.