11.3 MUnit 2.x Testing: Unit Tests, Mocking Connectors & Assertions
Key Takeaways
- MUnit 2.x is the native unit and integration testing framework for Mule 4, organized into XML test suites under `src/test/munit` and executed via Anypoint Studio or the `munit-maven-plugin` in CI/CD pipelines.
- Every `<munit:test>` follows a three-phase lifecycle: `<munit:behavior>` (preconditions, mocks, spies), `<munit:execution>` (event setup and flow invocation), and `<munit:validation>` (assertions and call verifications).
- The `<munit-tools:mock-when>` processor intercepts calls to external connectors (such as HTTP Request or Database) and substitutes simulated payloads, attributes, or simulated errors without invoking backend systems.
- The `<munit-tools:verify-call>` processor validates the exact execution frequency of a processor (e.g., verifying a database insert was called `times='1'` or an alert processor was called `times='0'`).
- MUnit coverage reports calculate flow, file, and application processor coverage, enabling automated build failure enforcement via Maven quality gates when thresholds are unmet.
MUnit 2.x Testing: Unit Tests, Mocking Connectors & Assertions
MUnit 2.x is the native testing framework designed specifically for Mule 4 applications. Fully integrated with Anypoint Studio and Apache Maven, MUnit allows developers to write automated unit and integration tests for flows, sub-flows, and error handlers. By mocking external endpoints, spying on intermediate message states, and verifying execution paths, MUnit ensures high software quality and prevents regressions in CI/CD automation.
1. MUnit 2.x Architecture & Project Structure
MUnit tests are authored as standard Mule XML configuration files located in the project's src/test/munit directory. Test artifacts, sample payloads, and mock responses are stored in src/test/resources.
+-------------------------------------------------------------------------+
| MUNIT PROJECT STRUCTURE |
| |
| my-mule-app/ |
| ├── src/main/mule/ <-- Application Flow XML Files |
| │ ├── orders-api.xml |
| │ └── global-configs.xml |
| ├── src/main/resources/ <-- Application Properties & Log4j 2 |
| ├── src/test/munit/ <-- MUnit Test Suite XML Files |
| │ ├── orders-api-test-suite.xml |
| │ └── error-handling-test-suite.xml |
| ├── src/test/resources/ <-- Mock Payloads, Samples, Schemas |
| │ ├── sample-order-request.json |
| │ └── mock-db-customer-response.json |
| └── pom.xml <-- munit-maven-plugin & Dependencies|
+-------------------------------------------------------------------------+
Lifecycle Scopes in an MUnit Test Suite:
<munit:before-suite>/<munit:after-suite>: Executes once before/after all test cases in the suite file (ideal for spinning up embedded test brokers or database fixtures).<munit:before-test>/<munit:after-test>: Executes before/after each individual<munit:test>case in the file (ideal for resetting variables or cleaning temporary state).<munit:test>: Defines an individual automated test case.
2. The Three-Phase Test Lifecycle (<munit:test>)
Every MUnit test case is organized into three distinct, sequential execution sections:
+-------------------------------------------------------------------------+
| MUNIT 3-PHASE TEST LIFECYCLE |
| |
| +-----------------------------------------------------------------+ |
| | 1. BEHAVIOR (<munit:behavior>) | |
| | - Configure Mock processors (<munit-tools:mock-when>) | |
| | - Configure Spies (<munit-tools:spy>) | |
| | - Define preconditions and stubbed return values | |
| +-----------------------------------------------------------------+ |
| | |
| v |
| +-----------------------------------------------------------------+ |
| | 2. EXECUTION (<munit:execution>) | |
| | - Initialize Event (<munit:set-event>) | |
| | - Invoke target flow under test (<flow-ref>) | |
| +-----------------------------------------------------------------+ |
| | |
| v |
| +-----------------------------------------------------------------+ |
| | 3. VALIDATION (<munit:validation>) | |
| | - Assert payload & variables (<munit-tools:assert-that>) | |
| | - Verify execution counts (<munit-tools:verify-call>) | |
| +-----------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
Comprehensive Example of an MUnit Test Case:
<munit:test name='createOrder-validPayload-returns201'
description='Verify that valid order submission creates record and returns HTTP 201'
doc:id='test-001'>
<!-- PHASE 1: BEHAVIOR -->
<munit:behavior>
<!-- Mock the Database Insert Processor -->
<munit-tools:mock-when processor='db:insert'>
<munit-tools:with-attributes>
<munit-tools:with-attribute attributeName='doc:name' whereValue='Insert Order Record' />
</munit-tools:with-attributes>
<munit-tools:then-return>
<munit-tools:payload value='#[{ affectedRows: 1 }]' mediaType='application/java' />
</munit-tools:then-return>
</munit-tools:mock-when>
</munit:behavior>
<!-- PHASE 2: EXECUTION -->
<munit:execution>
<!-- Construct Inbound Mule Event -->
<munit:set-event doc:name='Set Initial Request Event'>
<munit:payload value="#[MunitTools::getResourceAsString('sample-order-request.json')]"
mediaType='application/json' />
<munit:attributes value="#[{ headers: { 'client_id': 'test-client-123' } }]" />
</munit:set-event>
<!-- Trigger Flow Under Test -->
<flow-ref name='post:\orders:api-config' />
</munit:execution>
<!-- PHASE 3: VALIDATION -->
<munit:validation>
<!-- Assert Resulting Payload Status -->
<munit-tools:assert-that expression='#[payload.status]'
is="#[MunitTools::equalTo('CREATED')]"
message='Expected order status to be CREATED' />
<!-- Assert HTTP Status Code Attribute -->
<munit-tools:assert-that expression='#[vars.httpStatus]'
is='#[MunitTools::equalTo(201)]'
message='Expected HTTP Status code 201' />
<!-- Verify the DB Insert was called exactly once -->
<munit-tools:verify-call processor='db:insert' times='1'>
<munit-tools:with-attributes>
<munit-tools:with-attribute attributeName='doc:name' whereValue='Insert Order Record' />
</munit-tools:with-attributes>
</munit-tools:verify-call>
</munit:validation>
</munit:test>
3. Core Mocking, Spying & Verification Processors
| Processor | Placement Scope | Primary Purpose | Key Attributes / Child Elements |
|---|---|---|---|
<munit-tools:mock-when> | <munit:behavior> | Intercepts a message processor and returns stubbed data or errors without invoking external backends. | processor, <with-attributes>, <then-return> (payload, attributes, variables, errorType) |
<munit-tools:spy> | <munit:behavior> | Inspects the Mule event immediately before and immediately after a processor executes. | processor, <before-call> (assertions), <after-call> (assertions) |
<munit-tools:verify-call> | <munit:validation> | Verifies how many times a specific processor was executed during the test run. | processor, times='N', atLeast='N', atMost='N' |
<munit:set-event> | <munit:execution> | Constructs a simulated inbound Mule event with custom payload, attributes, and variables. | <payload>, <attributes>, <variables> |
<munit-tools:assert-that> | <munit:validation> | Evaluates a DataWeave expression against a Hamcrest matcher condition. | expression, is, message |
<munit-tools:assert-equals> | <munit:validation> | Compares actual value against expected value for exact equality. | actual, expected, message |
4. Mocking Exceptions & Testing Error Handlers
MUnit allows testing error handling flows without intentionally sabotaging external networks. By configuring <munit-tools:mock-when> to throw an error, developers simulate any Mule error type (e.g., HTTP:CONNECTIVITY, DB:BAD_SQL_SYNTAX, APP:INVALID_ACCOUNT).
<!-- Testing Error Handling with Mocked Exception -->
<munit:test name='postOrder-dbDown-handlesConnectivityError' doc:id='test-002'>
<munit:behavior>
<!-- Mock DB to throw HTTP:CONNECTIVITY error -->
<munit-tools:mock-when processor='db:insert'>
<munit-tools:then-return>
<munit-tools:error errorTypeId='DB:CONNECTIVITY' />
</munit-tools:then-return>
</munit-tools:mock-when>
</munit:behavior>
<munit:execution>
<munit:set-event>
<munit:payload value="#[{ orderId: 'ORD-123', total: 99.00 }]" mediaType='application/json' />
</munit:set-event>
<flow-ref name='post:\orders:api-config' />
</munit:execution>
<munit:validation>
<!-- Verify on-error-continue transformed payload to graceful 503 error message -->
<munit-tools:assert-equals actual='#[vars.httpStatus]' expected='#[503]' />
<munit-tools:assert-that expression='#[payload.error.code]'
is="#[MunitTools::equalTo('SERVICE_UNAVAILABLE')]" />
</munit:validation>
</munit:test>
Handling on-error-propagate with expectedErrorType:
If the target flow uses on-error-propagate, the error is re-thrown by the flow reference. In MUnit, unhandled re-thrown errors will fail the test unless the test explicitly declares expectedErrorType:
<munit:test name='validateOrder-invalidPayload-throwsCustomError'
expectedErrorType='ORDER:INVALID_PAYLOAD'>
<munit:execution>
<munit:set-event>
<munit:payload value='#[{ badData: true }]' mediaType='application/json' />
</munit:set-event>
<flow-ref name='validate-order-flow' />
</munit:execution>
</munit:test>
5. Hamcrest Matchers Reference (MunitTools::)
MUnit includes built-in Hamcrest matchers accessed via the MunitTools:: DataWeave module in <munit-tools:assert-that>:
| Matcher Function | Example Expression | Description |
|---|---|---|
equalTo(value) | is="#[MunitTools::equalTo('ACTIVE')]" | Asserts exact structural and value equality. |
not(matcher) | is='#[MunitTools::not(MunitTools::equalTo(0))]' | Inverts the specified matcher logic. |
nullValue() | is='#[MunitTools::nullValue()]' | Asserts that the expression evaluates to null. |
notNullValue() | is='#[MunitTools::notNullValue()]' | Asserts that the expression is not null. |
hasSize(number) | is='#[MunitTools::hasSize(5)]' | Asserts array or collection element count. |
hasItem(value) | is="#[MunitTools::hasItem('ADMIN')]" | Asserts collection contains the specified item. |
everyItem(matcher) | is='#[MunitTools::everyItem(MunitTools::notNullValue())]' | Asserts every item in collection matches condition. |
containsString(str) | is="#[MunitTools::containsString('Success')]" | Asserts target string contains substring. |
startsWith(str) | is="#[MunitTools::startsWith('ORD-')]" | Asserts string begins with target prefix. |
isEmpty() | is='#[MunitTools::isEmpty()]' | Asserts collection, string, or map is empty. |
6. MUnit Coverage Reports & Maven Quality Gates
Automated CI/CD pipelines use the munit-maven-plugin in pom.xml to execute test suites and generate code coverage reports.
+-------------------------------------------------------------------------+
| MUNIT COVERAGE METRICS |
| |
| 1. APPLICATION COVERAGE: |
| Percentage of all message processors executed across the entire |
| application during test suite execution. |
| |
| 2. RESOURCE / FILE COVERAGE: |
| Percentage of message processors executed within a specific XML |
| configuration file (e.g., orders-api.xml). |
| |
| 3. FLOW COVERAGE: |
| Percentage of message processors executed within an individual |
| Mule flow or sub-flow. |
+-------------------------------------------------------------------------+
Configuring Coverage Thresholds in pom.xml:
<plugin>
<groupId>com.mulesoft.munit.tools</groupId>
<artifactId>munit-maven-plugin</artifactId>
<version>2.3.14</version>
<executions>
<execution>
<id>test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
<goal>coverage-report</goal>
</goals>
</execution>
</executions>
<configuration>
<coverage>
<runCoverage>true</runCoverage>
<failBuild>true</failBuild>
<requiredApplicationCoverage>80</requiredApplicationCoverage>
<requiredResourceCoverage>75</requiredResourceCoverage>
<requiredFlowCoverage>70</requiredFlowCoverage>
<formats>
<format>html</format>
<format>json</format>
<format>console</format>
</formats>
</coverage>
</configuration>
</plugin>
- If
<failBuild>true</failBuild>is set and the calculated coverage falls below any configured threshold (requiredApplicationCoverage,requiredResourceCoverage, orrequiredFlowCoverage), the Maven build command (mvn clean test) terminates with a build failure, preventing deployment of untested code.
7. Exam Watch: Core MUnit Scenarios
[!IMPORTANT] Placement of
<munit-tools:mock-when>On the exam,<munit-tools:mock-when>and<munit-tools:spy>processors must always be placed inside the<munit:behavior>section. Placing mocks inside<munit:execution>or<munit:validation>is an invalid configuration.
[!WARNING] Testing
on-error-continuevson-error-propagate
- If a flow handles an exception with
on-error-continue, the flow completes successfully and returns a transformed message. Do not setexpectedErrorTypeon the<munit:test>.- If a flow handles an exception with
on-error-propagate, the error is re-thrown. The<munit:test>must declareexpectedErrorType='NAMESPACE:IDENTIFIER'to pass.
[!TIP] Verifying Conditional Branches with
times='0'Use<munit-tools:verify-call processor='smtp:send' times='0' />to verify that an alert email was never sent when testing a valid non-error execution path.
A Mule flow queries a backend customer database using a Database Select processor (processor='db:select') and transforms the result into JSON. A developer is writing an MUnit test to validate the transformation logic without connecting to the physical database. In which section of the <munit:test> should the developer place the <munit-tools:mock-when> component, and what must it return?
An order processing flow contains a Choice router that evaluates customer credit. If credit is approved, an external JMS Publish processor sends the order to a warehouse queue; if rejected, an email notification processor (processor='email:send') is invoked. When authoring an MUnit test for the 'credit rejected' scenario, how should the developer verify that no JMS message was published?
A developer needs to test a Mule flow that begins with an HTTP Listener and expects an inbound URI parameter orderId='ORD-5542' and an HTTP header authorization='Bearer token123'. What MUnit component should be placed inside <munit:execution> prior to the <flow-ref> to construct these inbound parameters?
A company enforces an automated CI/CD code quality policy requiring 80% application code coverage. During a Maven build (mvn clean test), the MUnit execution logs show that application processor coverage reached 74%, and the Maven build terminates with a BUILD FAILURE error. Which configuration in pom.xml caused the build to fail?