Part 2: Helpful Tools for Writing MUnit Tests

When writing MUnit tests we can borrow a lot from what has been learned from writing effective unit tests using other tools, such as JUnit. If the subject under test is a flow instead of a Java method, there are differences, and some interpretation is necessary. Depending on the applications, and the testing strategies used, MUnit tests can be complicated or simple.Part 2 of this two-part blog contains step-by-step instructions for adding MUnit tests for process flows. It will show you how to set up an MUnit test, provide input data for the test, mock out external references, and create both simple and complex assertions to ensure that the MuleSoft flow is working as expected. Using these instructions, you will be able to create MUnit tests that meet the objectives provided in Part 1 as quickly and easily as possible.

When writing MUnit tests, it’s important to start as early as possible. This will ensure the flows are designed for testability and that they perform properly for various scenarios defined by the tests you will create.  When you are done, your APIs will work as expected and contain sufficient tests to ensure they continue to work throughout the life-cycle of the API.

Step 1. Creating MUnit Test

Anypoint Studio has a feature that makes creating MUnit tests easy. Just right-click on the flow you want to test and click “create blank test for this flow”.

MUnit image

 

This will create your first new MUnit test in a new MUnit configuration file. When you create more tests, they will get created in the same configuration file as the first one. You can copy and paste the MUnit tests into different configuration files as needed. I recommend you do it from the Configuration XML tab since there have been issues with copy/pasting from the Message Flow tab. 

Take a look at the new MUnit flow. 

MUnit image 2

It has three sections, Execution, Behavior, and Validation. The Execution section allows you to set up the payload, variables, and attributes needed at the start of the test, and then starts execution of the test. The Behavior section allows you to add mock processors and spies. The Validation section is where you make assertions of the payload and variable data coming from the flow to ensure the flow behaved as expected. 

Step 2. Add Data for Your Test

Use the Set Event processor to add a payload, variables, or attribute values needed for your test. Drag and drop the Set Event processor to the execution section and before the flow reference as shown.

MUnit image 3

 

To add payload data, use a Dataweave script and set the media type to JSON, XML, or whatever is required by the flow under test. If your process needs variables or attributes, you can add them in a similar fashion. MUnit image 4

Step 3. Add Mock Services

If you are testing a flow that has external system calls (including a call to another Mule API)  or to sub-flows, then you can mock out those calls. Use the Mock when processor by dragging it from the palette to the behavior section of the MUnit test code as shown.  

MUnit image 5

 

Use the Pick processor button to find and add the processor you want to mock out. You should choose attributes that won’t change when selecting the processor, such as name or operation, since doc:id and doc:name may be changed. This is used by the MUnit test framework to attach the mock service to the real processor found in the flow.  Add as many Mock when processors as you need to the Behavior section. 

MUnit image 6

In most scenarios, you will want to return mock data. The Mock when a processor has a place to add a payload, attributes, and variable data to the process flow, in place of an actual service call. 

MUnit image 7

Adding Assertions and Using Diff to Compare Actual and Expected payloads

The Diff Module contains a helpful tool for MUnit tests. Instead of adding individual assertions for each value in a payload, we can test all the data in the payload against expected values at once. Take the example payload below. 

{

   "anArray":[{"no":1},{"no":3},{"no":3}],

   "anInteger": 2,

   "aString": "Hello"

}

 

It contains a bad result where the second array element contains a 3 instead 2. 

If you put the expected values in a variable and create a Dataweave script as shown,

%dw 2.0
import diff from dw::util::Diff output application/json var expected = {
"anArray":[{"no":1},{"no":2},{"no":3}], "anInteger": 2,

"aString": "Hello" } --- diff(payload,expected)

 

 

You can generate a useful report that can be used to assert an error. After this script is run, it returns information as shown below. You can keep this in the payload or put it in a variable, compared, for instance 

{

 "matches": false,

 "diffs": [

   {

     "expected": "2",

     "actual": "3",

     "path": "(root).anArray[1].no"

   }

 ]

}

 

 

In this example, a Set Variable processor in the Validation section of our MUnit test stores the report into the variable compared.

 

MUnit image 8

 

You can now create an assertion that the matches attribute has a value of true and if it is false, output the diffs array to the logs. 

MUnit image 9

 

If you run this test case where the array contains a bad value, you will get the following output from the MUnit test. This is very helpful in figuring out what is wrong since it contains many details about the error and what attribute failed. 

java.lang.AssertionError: Error in Payload, [

  {

    "expected": "2",

    "actual": "3",

    "path": "(root).anArray[1].no"

  }

] at file: [MUnit-test-suite.xml], line: [27]

Expected: "true"

     but: "false" as String {encoding: "UTF-8", mediaType: "application/json; charset=UTF-8", mimeType: 
"application/json", contentLength: 7} at (root) at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)

 

 

Note the expected and actual values and the path attribute,  “path”: “(root).anArray[1].no”

Using Spies

In the following example, the process flow changes the value of the payload mid-flow and again before the end of the flow. There isn’t a way to test the contents of the payload at the end of the flow, so we need to use a Spy processor.

 

MUnit image 10

The expected value in the payload before the database search is shown here. 

 

{ "$text":
{ "$search": "GoPro"}
}

 

This is the expected query used by this Mongo Find document processor to return a list of documents that contains “GoPro” in the product description. When the search returns, however, the Mongo results will overwrite the payload object with new data like this, for example: 

 


[

   {
       "_id": "5e0606db9ff305b4394e4318",
       
       "product_id": 1.0,
       
       "name": "GoPro HERO8",
       
       "manufacturer": "GoPro",
       
       "summary": "A Camera for exciting people",
       
       "description": "GoPro HERO8 Camera",
    },
    {
       "_id": "5e0606db9ff305b4394e4319",
       
       "product_id": 2.0,
       
       "name": "GoPro HERO7",
       
       "manufacturer": "GoPro",
       
       "summary": "A Camera for exciting people",
       
       "description": "GoPro HERO7 Camera",
       
    }
]



If you want to create a test for the query parameter in the payload, it is lost by the time the validation section is executed. By adding a Spy processor we can test the payload before it is over-written. Add a spy as shown and pick the database processor to spy on. 

 

MUnit image 11

 

You can configure the spy by adding an assertion in the Before call section of the spy. Note we can spy on payload, variable, or attribute data before the call is made or after. Here we add an assertion to the test.

MUnit image 12

 

This assertion tests that the query parameter located in the payload is as expected.

 


{ "$text":
{ "$search": "GoPro"}
}

 

Using Store and Remove/Retrieve for Safe Spy Assertions

Putting the assertion directly in the spy can be dangerous. If the spy doesn’t execute, the MUnit will pass when perhaps it shouldn’t. This can happen if the spy is not triggered or the flow doesn’t take the route expected and misses the processor entirely. To fix this we can store the data off in a memory hash so it can be retrieved, or removed, from the hash in the Validation section later on.   

 

MUnit image 13

 

In the spy example, we fix the code by storing the data, moving the assertions to the validation section, and then removing or retrieving during the validation step before the assertions.

 

MUnit image 14

Now if the spy doesn’t execute, the test will at least fail. It will fail because when we go retrieve the data in the validation section, it won’t be there and the assertion will fail.   

Testing Choice Routers Using Verify Call

When testing choice routers, for each loop, exception handling, or any flow where the path of execution can change, you can use Verify call. In the following process flow, there is a choice router with two conditional paths and a default path. You can test to be sure the default path or one of the other paths are chosen with the Verity call assertion. Let’s say you want to test that the path with the set payload due to Insufficient inventory is executed.  

 

MUnit image 15

Drag and drop the Verify call processor into the Validation section of the MUnit test.

MUnit image 16

 

Then configure the verify call process with the number of expected calls. Since this is in a choice router we expect one call only. For each loop, you may want the Verify call assertion to test that that call was made several times.

MUnit image 17

 

Although you can’t specify the error message, the error message you do get is good. In this example, the test failed because the expected number of calls was 1, but got 0 calls.

 

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

java.lang.AssertionError: On mule:set-payload. Expected 1 but got 0 calls

at org.mule.MUnit.mock.MockModule.verifyCall(MockModule.java:134)

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

ERROR 2020-03-16 17:57:36,133 [MUnit.01] org.mule.MUnit.runner.model.Suite: FAILURE - test: impl-patch-product-insufficient-inventory-test - Time elapsed: 0.25 sec

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Storing and using Test Data in Files 

Instead of putting large JSON or XML objects in your mule MUnit configuration files, you can reference the data in files and read them from the classpath into payload data, variables, and mock return values. 

Here’s an example using MUnitTools::getResourceAsString( )

 

%dw 2.0

output application/xml

---

read(MUnitTools::getResourceAsString("sample_data/your-file-name-here"),"application/xml")



Note:

If you are reading data into a Set Event or Mock return values, be sure to set the media type.

 

Here’s an example using the built-in function readUrl( )

 

%dw 2.0

import diff from dw::util::Diff

output application/json

var expected = readUrl('classpath://sample_data/your-file-name-here.xml',"application/xml")

---

diff(payload,expected)

 

getResourceAsString and readUrl can find the files if you store them under the src/test/resource directory. In these examples, we put the test files in a sample_data folder under src/test/resource.

Conclusions

There are many tools available to help in the creation of MUnit tests. These are the most common ones and they can help you get the most out of writing these tests. Combining this with the best practices described in Part 1, you will be able to meet the expectations of your peers and ensure the quality expectations are met for the stakeholders over the lifecycle of the API. 



6 minute read