Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

|
//

Mule ESB Testing (Part 2/3): Integration Testing and (Endpoint) Mocking with MUnit

26.1.2015 | 10 minutes of reading time

Abstract

As generally acknowledged testing is an important part of the software development process. Tests should be applied during each phase of the software development process from developer tests to acceptance tests. In software engineering comprehensive and automated test suits will secure the quality of software and can provide a safety net for regression and incompatibility changes.

In Mule ESB integration projects these same issues arise. Components used in Mule flows, the flows themselves and the integration of flows in a system context need to be tested thoroughly.

This article is the second one in a series of articles about testing Mule ESB projects on all levels. This article is focusing on the whole flows in a Mule project which are tested by combining already tested small components and sub flows in integration tests.

MUnit

MUnit is a Mule testing framework which was created originally as a side project in Mule but became open source later. It supports automation of Mule application testing for Mule developers. It is used widely in several companies and internal Mule projects [1 ].

You can find the project on GitHub: https://github.com/mulesoft/munit

Dependencies

Lets start with the required Maven dependencies for MUnit:

1<dependency>
2    <groupId>org.mule.munit</groupId>
3    <artifactId>munit-common</artifactId>
4    <version>${munit.version}</version>
5    <scope>test</scope>
6</dependency>
7<dependency>
8    <groupId>org.mule.munit</groupId>
9    <artifactId>munit-runner</artifactId>
10    <version>${munit.version}</version>
11    <scope>test</scope>
12</dependency>
13<dependency>
14    <groupId>org.mule.munit.utils</groupId>
15    <artifactId>munit-mclient-module</artifactId>
16    <version>${munit.version}</version>
17    <scope>test</scope>
18</dependency>
19<dependency>
20    <groupId>org.mule.munit</groupId>
21    <artifactId>munit-mock</artifactId>
22    <version>${munit.version}</version>
23    <scope>test</scope>
24</dependency>

Test case definition – XML vs. Java

When we build integration tests with MUnit we use technically their unit test infrastructure. Hence we have JUnit tests to perform unit tests for components and sub flows and integration tests for whole flows. These unit tests can be build in two ways with MUnit. By using an XML description which is called “Mule code” or by using a Java fluent API.

We present here the MUnit Hello World example [2 ] [3 ]. Assuming the following flow should be tested. Notice that this a typical sub flow, as there are no inbound-endpoints present:

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

“Mule code” testing such a flow would look like this:

1<!-- Load the config of MUnit -->
2<munit:config/>
3 
4<!-- Load the definition of the flow under test -->
5<spring:beans>
6 <spring:import resource="mule-config.xml"/>
7</spring:beans>
8 
9<!-- Define the test case -->
10<munit:test name="testingFlow"
11        description="We want to test that the flow always returns the same payload as we had before calling it.">
12 
13 <!-- Define the input of the test case -->
14 <munit:set payload-ref="#[string: Hello world!]"/>
15 <!-- Call the flow under test -->
16 <flow-ref name="echoFlow"/>
17 <!-- Assert the test result -->
18 <munit:assert-not-null/>
19 <munit:assert-that payloadIs-ref="#[string: Hello world!]"/>
20</munit:test>

The same example tested with the Mule Java fluent API requires a JUnit test which is derived from FunctionalMunitSuite and would look like this:

1public class FirstTest extends FunctionalMunitSuite {
2 
3 /**
4 * This can be omitted. In that case, the config resources will be taken from mule-deploy.properties file.
5 * @return The location of your MULE config file. 
6 */
7 @Override
8 protected String getConfigResources() {
9  return "mule-config.xml";
10 }
11 
12 @Test
13 public void testEchoFlow() throws Exception {
14  // Start the flow "echoFlow" with Mule event from testEvent(...) with the payload "Hello world!"
15  MuleEvent resultEvent = runFlow("echoFlow", testEvent("Hello world!"));
16 
17  // Get the payload result from the flow and assert the result
18  assertEquals("Hello world!", resultEvent.getMessage().getPayloadAsString());
19 }
20}

Please notice the overwrite of the protected String getConfigResources() method which provides a comma separated list of Mule and Spring XML files for the test. It should contain production flow definitions and test configurations to decouple production and test.

When it comes to the comparison of both approaches one can argue it is a matter of taste. This might be true for simple flows and test cases. Especially when using verification or spying (see below), one can argue that using XML is better readable. However, when we have a large test base with multiple test cases and many sub flows, reuse of test code becomes an issue. Therefore we prefer the Java approach over the XML approach because it allows easier reuse of helper classes, test configuration files and parent test classes. Therefore we will stick now with Java examples but be aware that there is always a XML style alternative as well.

Running a synchronous flow

As depicted in the last example this simple flow was started by using the protected final MuleEvent runFlow(String name, MuleEvent event) throws MuleException method of the FunctionalMunitSuite class. The important thing is to remember that even a flow which has an inbound endpoint by which it is started can be started this way, too.

Lets assume we have a a pricing service which integrates three suppliers for a wholesaler. It requests a price quote through the services and receives three results. The integration of these three suppliers could look like this:

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

To run this flow the protected final MuleEvent runFlow(String name, MuleEvent event) throws MuleException method needs to be provided with a MuleEvent . This can either be done by using the convenience method protected final MuleEvent testEvent(Object payload) throws Exception which creates a mule event or by creating it itself. The later provides additional control for the test case because the Mule message can be adapted explicitly e.g. by setting the Mule properties:

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

Mocking

When it comes to putting all tested components and sub flows together the task of testing their integration gets more and more complicated. Especially when other external systems which cannot be simulated are used in these flows. For integration-testing one of the greatest features of the MUnit framework is the ability of mocking all processors in a Mule flow [4 ]. This allows a thorough test of the overall flow.

To test this flow and the surrounding transformation, routing and other logic the inbound endpoint and three outbound calls can be mocked this way before the test is executed and asserted:

1@Test
2public void testPricingFlow() {
3 ...
4 // Mock the inbound processors to return a mocked input for the flow
5 whenMessageProcessor("inbound-endpoint")
6        .ofNamespace("http")
7        .thenReturn(mockedInboundMsg).getMessage());
8 
9 // Mock the outbound processors to return a mocked result
10 whenMessageProcessor("outbound-endpoint")
11        .ofNamespace("jms")
12        .thenReturn(muleMessageWithPayload("90 EUR").getMessage());
13 
14 whenMessageProcessor("outbound-endpoint")
15        .ofNamespace("http")
16        // Filter by message processor attributes of the endpoint
17        .withAttributes(attribute("host").withValue("100.55.32.*"))
18        .thenReturn(muleMessageWithPayload("100 EUR").getMessage());
19 
20 whenMessageProcessor("outbound-endpoint")
21        .ofNamespace("http")
22        // Filter by message processor attributes of the endpoint
23        .withAttributes(attribute("host").withValue(Matcher.contains("200.23.100.190")))
24        .thenReturn(muleMessageWithPayload("110 EUR").getMessage());
25 
26 // Mock a flow processors to return a the same event
27 whenMessageProcessor("send-business-notification")
28        .ofNamespace("notification")
29        .thenReturnSameEvent();
30 ...
31}

This way you have full control over the flow. We have the mocked inbound message which is returned to simulate the inbound endpoint and the mocked returned outbound messages which simulate the external calls.

For that purpose the class FunctionalMunitSuite provides the method whenMessageProcessor(String nameOfMessageProcessor) which returns an instance of MessageProcessorMocker mocking a specific processor. The mocked processor can be further specified by chaining attributes with the method public MessageProcessorMocker withAttributes(Attribute ... attributes). The public Attribute withValue(Object value) method can be used with the Matchers class or the any helper methods from the FunctionalMunitSuite class and provide a variety of control over the mocking. Furthermore even exception handling in flows can be tested with the public void thenThrow(Throwable exception) method.

Asserting, Verifying and Spying

For a thorough testing of the interior behavior of flows asserts of messages, verification of processor calls and spying on processors can be used [5 ] [6 ].

Basically messages can be asserted in Mule the classical Java way with JUnit asserts. This can be improved by using a matchers API such as Hamcrest or AssertJ which provides a fluent matchers API. We prefer the use of AssertJ because of our preference of fluent API’s.

For testing purposes MUnit provides a great way to verify the behavior of a flow. It provides a verification framework which allows to inspect processor calls after a test. When you have the above example and want to verify that all outbound endpoint were called you could do this at the end of a test by calling the following verification methods which also assert if the verification fails:

1@Test
2public void testPricingFlow() {
3 ...
4 // Verify JMS outbound endpoint was called one time
5 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("jms").times(1);
6 
7 // Verify HTTP outbound endpoint for supplier 1 was called one time
8 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("http")
9  .withAttributes(attribute("host").withValue("100.55.32.125")).times(1);
10 
11 // Verify HTTP outbound endpoint for supplier 2 was called one time
12 verifyCallOfMessageProcessor("outbound-endpoint").ofNamespace("http")
13  .withAttributes(attribute("host").withValue("200.23.100.190")).times(1);
14 ...
15}

Again by using the FunctionalMunitSuite class which has for that purpose the method verifyCallOfMessageProcessor(String nameOfMessageProcessor) which creates an instance of MunitVerifier the definition of the verification is done. The Attribute withValue(Object value) method in combination with public MunitVerifier withAttributes(Attribute ... attributes) method can be used to adapt the verification the same way as with mocking processors.

As an alternative to asserts at the end of the flow spying can be used to call code and include assert while running the flow . Let’s assume we want to verify that the input and output at the outbound processors is in a valid format:

1@Test
2public void testPricingFlow() {
3 ...
4 // Spy on the input and output of the processor
5 spyMessageProcessor("outbound-endpoint").ofNamespace("jms")
6  .before(new BeforeSpy())
7  .after(new AfterSpy());
8 
9 // Spy on the input and output of the processor
10 spyMessageProcessor("outbound-endpoint").ofNamespace("http")
11  .withAttributes(attribute("host").withValue("100.55.32.125"))
12  .before(new BeforeSpy())
13  .after(new AfterSpy());
14 
15 // Spy on the input and output of the processor
16 spyMessageProcessor("outbound-endpoint").ofNamespace("http")
17  .withAttributes(attribute("host").withValue("200.23.100.190"))
18  .before(new BeforeSpy())
19  .after(new AfterSpy());
20 ...
21}
22 
23private class BeforeSpy implements SpyProcess
24{
25 @Override
26 public void spy(MuleEvent event) throws MuleException
27 {
28  // Assert that the payload is a product code which is of type String and starts with PROD
29  assertThat(event.getMessage().getPayload()).isOfAnyClassIn(String.class);
30  assertThat(event.getMessage().getPayloadAsString()).startsWith("PROD");
31 }
32}
33 
34private class AfterSpy implements SpyProcess
35{
36 @Override
37 public void spy(MuleEvent event) throws MuleException
38 {
39  // Assert that the resulting payload is of type String and is a digit
40  assertThat(event.getMessage().getPayload()).isOfAnyClassIn(String.class);
41  assertThat(event.getMessage().getPayloadAsString()).matches("^\\d+$");
42 }
43}

As with mocking and verification we see here the same pattern which allows the definition of the spy processor. The FunctionalMunitSuite class has the method spyMessageProcessor(String name) which provides an instance of MunitSpy for the definition of the spying processor. Again it can be narrowed by using attributes. Using the methods before(final SpyProcess... withSpy) and after(final SpyProcess... withSpy) instances of a child class of SpyProcess can be added to be executed before and after a messages passes a messages processor during a test-run.

Running an asynchronous or polled flow

For testing asynchronous flows this additional dependency is required:

1<dependency>
2   <groupId>org.mule.modules</groupId>
3   <artifactId>munit-synchronize-module</artifactId>
4   <version>3.5-M1</version>
5   <scope>test</scope>
6</dependency>

It provides the Synchronizer class which contains a timeout infrastructure. The method process(MuleEvent event) throws Exception needs to be overwritten with a call to the asynchronous flow:

1@Test
2public void testPricingFlow() {
3 ...
4 Synchronizer synchronizer = new Synchronizer(muleContext, 20000l) {
5 
6  @Override
7  protected MuleEvent process(MuleEvent event) throws Exception {
8   runFlow("asyncPricingFlow", event);
9   return null;
10  }
11 };
12 
13 MuleEvent event = new DefaultMuleEvent(muleMessageWithPayload("PROD123"), MessageExchangePattern.ONE_WAY, MuleTestUtils.getTestFlow(muleContext));
14 synchronizer.runAndWait(event);
15 ...
16}

To assert the behavior the spying of the MUnit framework comes in handy. Just hang a spy class after the last processor or at another logical location to verify the result of the flow.

For tests on polling flows the test case needs first to deactivate polling ideally when the Mule context was created:

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

Afterwards the test data can be created, e.g. an in memory database can be populated which is polled by the flow. Then the polling can be triggered in the Synchronizer class afterwards:

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

Again the spy functionality can be used to verify the flow behavior. To assert that the result of an asynchronous or polled flow is correct a classic assert is performed after completion. In our example the test database can be queried and the result of the test can be verified.

Conclusion

We have shown in this article how integrations tests of multi module Mule applications can be performed. By using MUnit simple test cases can be build using a XML description or Java code itself. Processors such as outbound and inbound endpoints can be mocked in such tests and hence integrated flows can be thoroughly tested. By using asserts, verification and spying the correct behavior of a flow can be inspected and tested without touching production code or flow definition. Furthermore we depicted the additional changes to asynchronous and polled flows.

Series

This article is part of the Mule ESB Testing series:

References

[ 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

|

share post

Likes

0

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

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.