integralzone

MUnit Testing Done Right

Written by:
Published on April 1, 2021

MUnit Testing Done Right

Writing unit test cases is the key requirement of any MuleSoft implementation.

But in many organizations, you end up skipping to write the unit test cases in MuleSoft applications – since it is considered as too time-consuming or is too complex to write. And even in cases where test cases are written, they are generic MUnit test cases auto-generated by the studio – which won’t be updated and hence aren’t that useful. We are going to explore the proper way to implement a standard testing process in MuleSoft – which should bring down the effort way down and reap the benefits the testing is supposed to deliver.

Background


The essence of any unit testing is to make sure that all the aspects of the application/API generated is working as expected. This becomes extremely crucial in an API world because all the APIs in the enterprise should behave the same way (ie. The way APIs communicate error responses, common headers to be used, etc). Even though API specifications re-use a common API template for design, wouldn’t it make sense to have a common test case that can be re-used for the majority of API implementations – and require very minimal test case tweaking to get it completed?

Previously we had to write custom testing framework to accomplish this – but with the parameterized testing feature of MUnit (https://docs.mulesoft.com/munit/2.3/parameterized ), most of the custom implementation could be replaced with out of the box features. Let’s see how this works with an example!

Step 1: Understand the Enterprise API standards to be tested


Each customer ends up using different versions of API specification templates and in most of the cases, end up customizing the template to meet their needs. But the most common thing all enterprises will have is how the errors and API responses will be handled. For example, all APIs should have a common error structure and message format.

For the purpose of this blog, we will use a common structure for error message to look like below:

{  
  "header" : {  
    "apiName" : "mule-api-template",  
    "apiVersion" : "1.0.0",  
    "correlationId" : "9d940080-7b4e-11eb-8e34-acde48001122"  
  },  
  "errors" : [ {  
    "description" : "There was an issue with your request message. Required header 'x-correlation-id' not specified",  
    "code" : 400,  
    "timestamp" : "2021-03-02T11:58:35.302Z"  
  } ]  
}  

The following are the key error codes being used for representational purposes:

HTTP Response Code Description
200 Success for normal API calls
200 Success for API Consoles
400 Bad request
404 Not implemented
406 Not acceptable
415 Unsupported Media Type
500 Server Error
501 Method not allowed

We also add a custom mandatory HTTP header to all incoming requests – like X-Correlation-Id

And finally, we will re-use existing mule implementation template available at https://github.com/mulesoft-catalyst/api-template-mule4.x

Customization of error handler plugin is not covered as part of this post.

Step 2: Create base MUnit test case to test all the enterprise specifications in a generic manner


Now the fun bit. Create a simple test case that has all its features parameterized.

In this example below, we have set up a simple test case that does the following:

  1. Get payload from a parameterized file
  2. Call API as a request – with the required parameters
  3. Check the status code to be as expected
  4. Do additional checks based on how the test case needs to be customized for the organization

Let’s see some of the key configurations from the test case:

HTTP Requester component:

Status code assertion:

Please find below the sample test case implementation code:

1.	<?xml version="1.0" encoding="UTF-8"?>  
2.	  
3.	<mule xmlns:http="http://www.mulesoft.org/schema/mule/http"  
4.	    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:munit="http://www.mulesoft.org/schema/mule/munit"  
5.	    xmlns:munit-tools="http://www.mulesoft.org/schema/mule/munit-tools" xmlns="http://www.mulesoft.org/schema/mule/core"  
6.	    xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xsi:schemaLocation="  
7.	        http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd  
8.	        http://www.mulesoft.org/schema/mule/munit http://www.mulesoft.org/schema/mule/munit/current/mule-munit.xsd  
9.	        http://www.mulesoft.org/schema/mule/munit-tools  http://www.mulesoft.org/schema/mule/munit-tools/current/mule-munit-tools.xsd  
10.	http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd">  
11.	    <munit:config name="new-test-suite.xml" >  
12.	        <munit:parameterizations file="test-parameters.yaml" />  
13.	    </munit:config>  
14.	    <http:request-config name="New_HTTP_Request_configuration" doc:name="HTTP Request configuration" doc:id="77b91123-f998-43ae-a38f-df4166d6c310">  
15.	        <http:request-connection host="${http.host}" port="${http.port}" />  
16.	    </http:request-config>  
17.	    <munit:dynamic-port doc:name="MUnit dynamic port" doc:id="353d02e3-6e35-4e8d-a57e-f64f87ceebb1" propertyName="http.port" />  
18.	    <global-property doc:name="Global Property" doc:id="ee62529c-0cab-4465-8841-ef23b13c1d2e" name="mule.env" value="dev" />  
19.	    <global-property doc:name="Global Property" doc:id="15387adf-e4b9-42a6-8348-dccb7db6eb3d" name="mule.encryption.key" value="1234567812345678" />  
20.	    <munit:test name="global-api-test" doc:id="50c9db34-1d37-4536-8a8a-b3c9ec63e597" description="Test">  
21.	        <munit:enable-flow-sources >  
22.	            <munit:enable-flow-source value="api-template-console" />  
23.	            <munit:enable-flow-source value="api-template-main" />  
24.	            <munit:enable-flow-source value="post:\template-test:api-template-config" />  
25.	            <munit:enable-flow-source value="implementationFlow" />  
26.	        </munit:enable-flow-sources>  
27.	        <munit:execution>  
28.	            <set-payload value="#[output application/json --- readUrl(Mule::p('payloadUrl'))]" doc:name="Set Payload" doc:id="591eeb07-14be-4f48-bda6-b4d23e25dd49" mimeType="application/json"/>  
29.	            <http:request method="${method}" doc:name="Request" doc:id="a4b244ae-cd8a-41f0-a48b-150b16fc7cb3" config-ref="New_HTTP_Request_configuration" sendCorrelationId="#[if(sizeOf(p('correlationId'))>0)'ALWAYS' else 'NEVER']" path="${url}" outputMimeType="application/json" correlationId="${correlationId}">  
30.	                <http:headers ><![CDATA[#[output application/java 
31.	--- 
32.	{ 
33.	    "content-type" : p('contentType'), 
34.	    "accept" : p('accept') 
35.	}]]]></http:headers>  
36.	                <http:response-validator >  
37.	                    <http:success-status-code-validator values="#[${httpStatus}]" />  
38.	                </http:response-validator>  
39.	            </http:request>  
40.	        </munit:execution>  
41.	        <munit:validation >  
42.	            <munit-tools:assert-that expression="#[attributes.statusCode]" is="#[MunitTools::equalTo(${httpStatus})]" message="The HTTP Status code is not correct!" doc:name="Assert That Status Code is as expected" />  
43.	            <choice doc:name="Choice" doc:id="d1e78f92-22d8-4528-a057-711c938fba32" >  
44.	                <when expression="#[p('assertNonStatus') == 'true']">  
45.	                    <logger level="INFO" doc:name="Logger" doc:id="271da83a-4ff9-46b9-9ef1-3a39b228fbda" />  
46.	                    <munit-tools:assert-that doc:name="Assert that Correlation ID is as expected" doc:id="448e3ac6-0d92-4dc0-a4fb-ebeabab88a51" is="#[MunitTools::equalTo(if(sizeOf("${correlationId}") > 0) "${correlationId}" else attributes.'X-Correlation-Id')]" expression="#[attributes.headers.'X-Correlation-Id']" message="Correlation ID is not as expected!"/>  
47.	                    <munit-tools:assert-equals doc:name="Assert that error codes are as expected" doc:id="b7412c11-1870-4ab2-a400-a71cea38e293" actual="#[payload.errors[0].description]" expected="${description}" message="Error Codes are not matching!" />  
48.	                </when>  
49.	                <otherwise >  
50.	                    <logger level="INFO" doc:name="Logger" doc:id="33a4e9fb-fad7-498d-b1c5-cfe50843108a" message="Test Completed"/>  
51.	                </otherwise>  
52.	            </choice>  
53.	            </munit:validation>  
54.	    </munit:test>  
55.	  
56.	</mule>

Step 3: Create parameterized properties


Create a structure which works based on the test case you have created. For example, for the simple test case above, we are using the parameter that looks like below:

1.	200-success:  
2.	    url: "/api/v1/template-test"  
3.	    method: "POST"  
4.	    correlationId: "success-correlation-id"  
5.	    accept: "application/json"  
6.	    contentType: "application/json"  
7.	    httpStatus: "200"  
8.	    assertNonStatus: "false"  
9.	    description: ""  
10.	    payloadUrl: "classpath://set-event_payload.dwl" 

As can be noticed, the parameters set here are exactly what will be used in the test case message processors. Once we test for a single parameter, we can add all the different parameters to test out all the different combinations – for all the error types of the API.

Find below the sample configuration that you could use to simulate the key errors listed for this example:

1.	200-console:  
2.	    url: "/console"  
3.	    method: "GET"  
4.	    destinationType: "#docs"  
5.	    destinationName: "summary/summary"  
6.	    correlationId: "success-correlation-id"  
7.	    accept: "*/*"  
8.	    contentType: "application/json"  
9.	    httpStatus: "200"  
10.	    assertNonStatus: "false"  
11.	    errorCode: ""  
12.	    payloadUrl: "classpath://set-event_payload.dwl"  
13.	  
14.	200-success:  
15.	    url: "/api/v1/template-test"  
16.	    method: "POST"  
17.	    correlationId: "success-correlation-id"  
18.	    accept: "application/json"  
19.	    contentType: "application/json"  
20.	    httpStatus: "200"  
21.	    assertNonStatus: "false"  
22.	    description: ""  
23.	    payloadUrl: "classpath://set-event_payload.dwl"  
24.	  
25.	  
26.	400-bad-request-no-correlation-id:  
27.	    url: "/api/v1/template-test"  
28.	    method: "POST"  
29.	    correlationId: ""  
30.	    accept: "application/json"  
31.	    contentType: "application/json"  
32.	    httpStatus: "400"  
33.	    assertNonStatus: "false"  
34.	    description: "There was an issue with your request message. Required header 'x-correlation-id' not specified"  
35.	    payloadUrl: "classpath://set-event_payload.dwl"  
36.	  
37.	  
38.	404-not-implemented-url:  
39.	    url: "/api/v1/template-test2"  
40.	    method: "POST"  
41.	    correlationId: "success-correlation-id"  
42.	    accept: "application/json"  
43.	    contentType: "application/json"  
44.	    httpStatus: "404"  
45.	    assertNonStatus: "true"  
46.	    description: "The API has not been implemented /template-test2"  
47.	    payloadUrl: "classpath://set-event_payload.dwl"  
48.	            
49.	      
50.	      
51.	406-not-acceptable-accept:  
52.	    url: "/api/v1/template-test"  
53.	    method: "POST"  
54.	    correlationId: "success-correlation-id"  
55.	    accept: "application/xml"  
56.	    contentType: "application/json"  
57.	    httpStatus: "406"  
58.	    assertNonStatus: "true"  
59.	    description: "One of the request or parameters is unacceptable Not Acceptable"  
60.	    payloadUrl: "classpath://set-event_payload.dwl"   
61.	  
62.	      
63.	415-unsupported-media-type:  
64.	    url: "/api/v1/template-test"  
65.	    method: "POST"  
66.	    correlationId: "success-correlation-id"  
67.	    accept: "application/json"  
68.	    contentType: "application/xml"  
69.	    httpStatus: "415"  
70.	    assertNonStatus: "true"  
71.	    description: "Media Type not supported Unsupported mediaType"  
72.	    payloadUrl: "classpath://set-event_payload.dwl"  
73.	      
74.	      
75.	500-server-error:  
76.	    url: "/api/v1/template-test"  
77.	    method: "POST"  
78.	    correlationId: "success-correlation-id"  
79.	    accept: "application/"  
80.	    contentType: "application/json"  
81.	    httpStatus: "500"  
82.	    assertNonStatus: "true"  
83.	    description: "Internal Server Error"  
84.	    payloadUrl: ""      
85.	      
86.	  
87.	      
88.	501-method-not-allowed:  
89.	    url: "/api/v1/template-test"  
90.	    method: "GET"  
91.	    correlationId: "success-correlation-id"  
92.	    accept: "application/json"  
93.	    contentType: "application/json"  
94.	    httpStatus: "501"  
95.	    assertNonStatus: "true"  
96.	    description: "The method has not been implemented HTTP Method get not allowed for : /template-test"  
97.	    payloadUrl: "classpath://set-event_payload.dwl"  

Step 4: Include the sample test case as part of your mule template


Organizations should ideally be following (and hopefully most are) following a set mule template which simplifies by putting all the best practices into a template – which should reduce more than 60% of all the implementation effort. See an example of such a mule code template at https://github.com/mulesoft-catalyst/api-template-mule4.x

Put the generated enterprise test case as part of this template – so that every project which implements using this template will have the generic test case added as part of it.

Step 5: Customize the test case based on API being implemented


Once the mule application is generated based on the code template, a base standard test case will already be present in the implementation. The only things you would need to change would be which resources to call and what error codes to test for. Note that even these can be automated (and should be in large organizations!) – which is out of scope of this article. Let’s say the API being implemented has a call to Anypoint MQ (or any other connector/system), you could add API specific behavior like below:

In this use case, we are conditionally mocking the Anypoint MQ connector – to provide success message for certain scenarios and to throw specific errors for other scenarios. In other words, the test cases can be customized as you need it for any use case being implemented.

When you run the test case you should now see the test case running for every parameter like below:

You can check the coverage of the test cases:

Effort involved in customization should be 80-90% less than creating/generating basic test cases – since everything is templated! Another benefit of this approach would be that you could even govern and track compliance using a product like IZ Analyzer (https://integralzone.com/products/iz-analyzer/ ).

Parting thoughts


MuleSoft continues to enhance its existing features regularly to incorporate best practices and standards to suit wider group of customers and organizations. One of the drawbacks of such a fast-paced innovation is the fact that many of the features will either go unnoticed or lack documentation/tutorials to highlight the actual benefits the feature brings to the table. Architects and Leads should always strive to keep themselves up-to-date with all the major features being added to make sure that we can bring the benefits back to the organizations! This holds true especially for features like MUnit which is perceived to be the hardest and most troublesome by most developers. Hopefully, this feature will ease the burden of the developers by 70-80% and help in boosting productivity and increase compliance!

Thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *

Other Blog Posts

Other Blog Posts

How to ship high-quality code faster

All software is built using code. Today, the software we’ve come to rely on in our everyday lives is ubiquitous across our society, at home, and at work. Without fail, the software and applications gaining the most adoption and stickiness, are the ones that rise to success through rapid innovation, and superior user experience. So, …

Read more

Developing GraphQL in MuleSoft

GraphQL benefits and whether it trumps API or works alongside it is a discussion for another blog post. This blog post takes a very quick walk-through of the GraphQL router from MuleSoft – aimed at developers to quickly get things up and running and try it out for themselves. Step 1: Install GraphQL Router GraphQL …

Read more

Mule Code Analysis with Azure Dev Ops

Azure Dev Ops is one of the leading Dev Ops tools of choice for most organizations trying to cover the entire application life cycle. One of the key requirements for Dev Ops tools is to integrate and provide a way to maintain MuleSoft code quality and security for the applications going through the Dev Ops …

Read more