Testing error handling scenarios is a critical aspect of building robust and resilient MuleSoft applications.
When an application interacts with external systems via the HTTP connector, it’s not a matter of if an error will occur, but when. Therefore, it’s essential to verify that your application can gracefully handle various error responses, such as 4xx client errors or 5xx server errors, and follow the designed compensation logic.
The challenge in testing these paths lies in accurately replicating the complex error object that the MuleSoft HTTP connector generates, which contains a nested structure including status codes, headers, and a payload.
{
"exceptionPayload": null,
"payload": {
"message": "Account already exists!"
},
"attributes": {
"headers": {
"x-correlation-id": "f62f3aad-2d95-42ff-8cd1-6cbba8c8410a",
"host": "localhost:6075",
"user-agent": "AHC/1.0",
"connection": "close",
"accept": "*/*",
"content-type": "application/json; charset=UTF-8",
"content-length": "42",
"date": "Sat, 27 Sep 2025 21:37:36 GMT"
},
"reasonPhrase": "Bad Request",
"statusCode": 400
}
}This guide provides a detailed analysis of approaches for mocking and testing HTTP error responses within MUnit.
It serves as a reference, offering deep insights into the mechanics, advantages, and disadvantages of each method. The goal is to equip you with the knowledge to choose and implement the most effective and maintainable testing strategy for your projects.
Here a summary of the four options:
-
Option A: A reusable and high-fidelity approach using a live HTTP listener on a dynamic port to generate realistic, fully-formed error objects. This is the best solution and is well explained.
-
Option B: A basic, direct approach that tests for expected Mule errors or specific HTTP status codes without needing to mock the connector’s internal behavior.
-
Option C: A more complex variation of Option A that uses a multi-step internal HTTP call structure to achieve a similar result.
-
Option D: A self-contained approach that uses DataWeave to manually construct a Java Exception object, bypassing the need for a live listener. This option will work, but the
error.errorTypewill not complain with the expectederror.errorType(namespace MULE / identifier UNKNOWN), and it will need a few extra steps to avoid issues in the IDE Anypoint Studio that may show some error in the files.
We will conclude with a detailed comparative analysis and a step-by-step tutorial for implementing the recommended best practice, Option A, which offers the best balance of realism and reusability.
The base for most of the options (A,B,C or D) is the feature enabled flows and then-call in each munit test.
<munit:enable-flow-sources>
<munit:enable-flow-source
value="munit-util-mock-http-error-with-errorMessage-test-suite.http-listener-for-mock-responses" />
<munit:enable-flow-source
value="munit-util-mock-http-error-with-errorMessage-test-suite.trigger-mock-http-request" />
</munit:enable-flow-sources><munit-tools:then-call
flow="impl-test-suite.mock-http-req-external-400.flow" />So, it’s important to enable the flows (Listener or any other) that will be used in the test and for each Mock it will have a Flow in then call option
ℹ️ You may think at Import the file that has the Flows that are used in the MUnit tests, but this doesn’t work very well during the MUnit Test Run, so avoid this approach:
<import
doc:name="Import"
file="option-a\munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml"
doc:description="munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml" />Otherwise will get an error like:
org.mule.runtime.api.exception.MuleRuntimeException: org.mule.runtime.core.api.config.ConfigurationException: [option-a/munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml:27; option-a\munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml:27]: Two (or more) configuration elements have been defined with the same global name. Global name 'MUnit_HTTP_Listener_config' must be unique.
'munit-util-mock-http-error-with-errorMessage-test-suite.http-listener-for-mock-responses' must be unique.
[option-a/munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml:67; option-a\munit-util-mock-http-request-for-errorMessage-using-listener-localhost-test-suite.xml:67]:One or more options may use dynamic port feature from MUnit, the official documentation is available on Dynamic Ports | MuleSoft Documentation. Feel free to take a look.
This section provides a detailed breakdown of each of the four testing strategies.
This is the recommended approach for its balance of realism, reusability, and fine-grained control over the mocked error.
See also this Stack Overflow question related to this approach: https://stackoverflow.com/questions/78878885/munits-and-error-handling-how-to-mock-error-error-mulemessage
The core idea is to intercept an outbound http:request using a mock-when processor and, instead of returning a simple value, redirect the execution to a utility flow within the MUnit test suite using then-call.
This utility flow then makes a real HTTP request to a real HTTP listener that is also running as part of the MUnit test on a dynamic port.
This listener is strategically configured to generate a specific HTTP error response (status code, payload, headers). The HTTP connector within the utility flow receives this error response and naturally throws a standard Mule error, which is then propagated back through the mock.
This process creates a highly realistic, fully-structured error object for your test to validate, perfectly mimicking how the connector behaves in a production environment.
sequenceDiagram
participant Test as MUnit Test
participant Flow as Flow Under Test
participant Mock as Mock When (http:request)
participant Util as Flow to Mock HTTP Response
participant Listener as MUnit HTTP Listener
Test->>Flow: Execute Flow Ref
Flow->>Mock: HTTP Request to external system
Mock->>Util: then-call utility flow
Util->>Listener: Makes REAL HTTP request
Listener-->>Util: Responds with error (e.g., 400 Bad Request + payload)
Util-->>Mock: Propagates HTTP Connector error
Mock-->>Flow: Throws realistic error object
Flow->>Flow: Enters on-error-continue/propagate scope
Test->>Flow: Verify behavior in error handler
The implementation utilizes two main flows that can be reused for each munit test case, it’s important to mention that for each HTTP Request that you want to mock as error you will need to create or reference a respective flow that defines the structure (status code, payload, headers) you want to thrown.
<mule ...>
<munit:config name="impl-option-a-test-suite.xml" />
<!-- 1. A dynamic port is reserved for the test listener to avoid conflicts. -->
<munit:dynamic-port
propertyName="munit.dynamic.port"
min="6000"
max="7000" />
<!-- 2. The listener runs on the dynamic port defined above. -->
<http:listener-config
name="MUnit_HTTP_Listener_config"
doc:name="HTTP Listener config">
<http:listener-connection
host="0.0.0.0"
port="${munit.dynamic.port}" />
</http:listener-config>
<!-- This request config targets the local listener. -->
<http:request-config name="MUnit_HTTP_Request_configuration">
<http:request-connection
host="localhost"
port="${munit.dynamic.port}" />
</http:request-config>
<!-- 3. This flow acts as the mock server. It receives requests from the utility flow and generates the desired HTTP response. -->
<flow name="munit-util-mock-http-error.listener">
<http:listener
doc:name="Listener"
config-ref="MUnit_HTTP_Listener_config"
path="/*">
<http:response
statusCode="#[(attributes.queryParams.statusCode default attributes.queryParams.httpStatus) default 200]"
reasonPhrase="#[attributes.queryParams.reasonPhrase]">
<http:headers>
<![CDATA[#[attributes.headers]]]>
</http:headers>
</http:response>
<http:error-response
statusCode="#[(attributes.queryParams.statusCode default attributes.queryParams.httpStatus) default 500]"
reasonPhrase="#[attributes.queryParams.reasonPhrase]">
<http:body>
<![CDATA[#[payload]]]>
</http:body>
<http:headers>
<![CDATA[#[attributes.headers]]]>
</http:headers>
</http:error-response>
</http:listener>
<logger
level="TRACE"
doc:name="doc: Listener Response will Return the payload/http status for the respective request that was made to mock" />
<!-- The listener simply returns whatever payload it received, but within an error response structure. -->
</flow>
<!-- 4. This is the reusable flow called by 'then-call'. Its job is to trigger the listener. -->
<flow name="munit-util-mock-http-error.req-based-on-vars.munitHttp">
<try doc:name="Try">
<http:request
config-ref="MUnit_HTTP_Request_configuration"
method="#[vars.munitHttp.method default 'GET']"
path="#[vars.munitHttp.path default '/']"
sendBodyMode="ALWAYS">
<!-- It passes body, headers and query params from a variable, allowing dynamic control over the mock's response. -->
<http:body>
<![CDATA[#[vars.munitBody]]]>
</http:body>
<http:headers>
<![CDATA[#[vars.munitHttp.headers default {}]]]>
</http:headers>
<http:query-params>
<![CDATA[#[vars.munitHttp.queryParams default {}]]]>
</http:query-params>
</http:request>
<!-- The error generated by the listener is naturally propagated back to the caller of this flow. -->
<error-handler>
<on-error-propagate doc:name="On Error Propagate">
<!-- Both error or success will remove the variables for mock, so it doesn't mess with the next operation in the flow/subflow that are being tested. -->
<remove-variable
doc:name="munitHttp"
variableName="munitHttp" />
<remove-variable
doc:name="munitBody"
variableName="munitBody" />
</on-error-propagate>
</error-handler>
</try>
<remove-variable
doc:name="munitHttp"
variableName="munitHttp" />
<remove-variable
doc:name="munitBody"
variableName="munitBody" />
</flow>
<munit:test
name="impl-test-suite-impl-sub-flowTest"
timeOut="900000">
<!-- 5. Critical Step: You must enable the utility flows so they can be discovered and called by the MUnit runtime. -->
<munit:enable-flow-sources>
<munit:enable-flow-source
value="munit-util-mock-http-error.req-based-on-vars.munitHttp" />
<munit:enable-flow-source
value="munit-util-mock-http-error.listener" />
</munit:enable-flow-sources>
<munit:behavior>
<!-- -->
<munit-tools:mock-when
doc:name="Mock HTTP Req External -> then call flow 400 ;"
processor="http:request">
<munit-tools:with-attributes>
<!-- Identify the specific http:request instance to intercept. -->
<munit-tools:with-attribute
whereValue="GET"
attributeName="method" />
<munit-tools:with-attribute
whereValue="http://example.com/external"
attributeName="url" />
</munit-tools:with-attributes>
<munit-tools:then-call
flow="impl-test-suite.mock-http-req-external-400.flow" />
</munit-tools:mock-when>
<!-- -->
<munit-tools:mock-when
doc:name="Mock HTTP Req System -> then call flow 503 ;"
processor="http:request">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="GET"
attributeName="method" />
<munit-tools:with-attribute
whereValue="http://example.com/system"
attributeName="url" />
</munit-tools:with-attributes>
<!-- 6. Instead of returning a value, instruct the mock to call our setup flow. -->
<munit-tools:then-call
flow="impl-test-suite.mock-http-req-system-503.flow" />
</munit-tools:mock-when>
<!-- -->
<munit-tools:spy
doc:name="Spy HTTP Req System GET /health"
processor="http:request">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="GET"
attributeName="method" />
<munit-tools:with-attribute
whereValue="HTTP_Request_configuration_System"
attributeName="config-ref" />
<munit-tools:with-attribute
whereValue="/health"
attributeName="path" />
</munit-tools:with-attributes>
</munit-tools:spy>
<!-- -->
<munit-tools:mock-when
doc:name="Mock HTTP Req Process -> then call flow (default 200) ;"
processor="http:request">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="GET"
attributeName="method" />
<munit-tools:with-attribute
whereValue="http://example.com/process"
attributeName="url" />
</munit-tools:with-attributes>
<munit-tools:then-call
flow="munit-util-mock-http-error.req-based-on-vars.munitHttp" />
</munit-tools:mock-when>
</munit:behavior>
<!-- -->
<munit:execution>
<flow-ref
doc:name="Flow-ref to impl-for-option-a.subflow"
name="impl-for-option-a" />
</munit:execution>
<!-- -->
<munit:validation>
<munit-tools:verify-call
doc:name="ERROR EXCEPTION Req External"
processor="logger"
atLeast="1">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="ERROR EXCEPTION Req External"
attributeName="doc:name" />
</munit-tools:with-attributes>
</munit-tools:verify-call>
<!-- -->
<munit-tools:verify-call
doc:name="ERROR EXCEPTION Req System"
processor="logger"
atLeast="1">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="ERROR EXCEPTION Req System"
attributeName="doc:name" />
</munit-tools:with-attributes>
</munit-tools:verify-call>
<!-- -->
<munit-tools:verify-call
doc:name="3x HTTP Req MUnit Listener"
processor="http:request"
times="3">
<munit-tools:with-attributes>
<munit-tools:with-attribute
whereValue="MUnit_HTTP_Request_configuration"
attributeName="config-ref" />
</munit-tools:with-attributes>
</munit-tools:verify-call>
</munit:validation>
</munit:test>
<!-- 7. This flow acts as a test-specific setup, preparing the data for the mock. -->
<flow name="impl-test-suite.mock-http-req-external-400.flow">
<ee:transform
doc:name="munitHttp {queryParams: statusCode: 400 } } ; munitBody ;"
doc:id="904f4a7e-b23d-4aed-a4e1-f049c97434ef">
<ee:message></ee:message>
<ee:variables>
<!-- This variable will become the body of the error response. -->
<ee:set-variable variableName="munitBody">
<![CDATA[%dw 2.0 output application/json --- { message: "Account already exists!" }]]>
</ee:set-variable>
<!-- This variable passes the desired status code to the listener via query parameters. -->
<ee:set-variable variableName="munitHttp">
<![CDATA[%dw 2.0 output application/java ---
{
path : "/",
method: "GET",
queryParams: {
statusCode: 400,
},
}]]>
</ee:set-variable>
</ee:variables>
</ee:transform>
<!-- 8. Finally, call the reusable utility flow to trigger the mock listener. -->
<flow-ref
doc:name="FlowRef req-based-on-vars.munitHttp-flow"
name="munit-util-mock-http-error.req-based-on-vars.munitHttp" />
</flow>
<flow name="impl-test-suite.mock-http-req-system-503.flow">
<ee:transform
doc:name="munitHttp {queryParams: statusCode: 503 } } ; munitBody ;"
doc:id="de07920c-9cbc-4a52-aa8b-81fe4de93229">
<ee:message></ee:message>
<ee:variables>
<ee:set-variable variableName="munitHttp">
<![CDATA[%dw 2.0
output application/java
---
{
path : "/",
method: "GET",
queryParams: {
statusCode: 503,
},
}]]>
</ee:set-variable>
<ee:set-variable variableName="munitBody">
<![CDATA[%dw 2.0
output application/json indent=false
---
{
message: ""
}]]>
</ee:set-variable>
</ee:variables>
</ee:transform>
<!-- -->
<flow-ref
doc:name="FlowRef req-based-on-vars.munitHttp-flow"
name="munit-util-mock-http-error.req-based-on-vars.munitHttp" />
</flow>
</mule>-
High Fidelity: Generates a true
error.errorMessageobject, complete with attributes (statusCode, headers) and payload. This is crucial for accurately testing on-error scopes that inspect these details, for instance:when="#[error.errorMessage.attributes.statusCode == 404]". -
Reusable: The utility listener and requester flows can be defined once in the same MUnit Test Suite file, promoting a DRY (Don’t Repeat Yourself) testing principle. ℹ️ an isolate and different common file didn’t worked for reuse across hundreds of test suites
-
Flexible: It’s trivial to configure different status codes, payloads, and headers on a per-test basis by simply changing the
munitHttpandmunitBodyvariable in the test-specific setup flow. -
Maintainable: This pattern cleanly separates the test setup logic (what the mock should do) from the test execution and validation, making individual tests much cleaner and easier to understand.
-
Initial Setup: Requires more upfront configuration compared to simpler methods. However, this is a one-time investment for a highly reusable test utility.
-
Complexity: The interaction between multiple flows (
mock-when→ setup flow → utility flow → listener flow) can be slightly harder for developers new to MUnit to grasp initially.
|
Note
|
Error:
This is a common error in MUnit tests. It happens when your test tries to call a flow that the MUnit runtime has not started.
Referenced component '…' must be one of stereotypes [MULE:FLOW, MULE:SUB_FLOW] |
By default, MUnit only starts the main flow that is being explicitly tested. If your test code uses a flow-ref or a similar component to call an auxiliary flow (like a utility flow or a mocked listener), the test will fail because that other flow isn’t running.
You need to explicitly tell MUnit to start all required flows for your test.
-
In your test case, add the
<munit:enable-flow-sources>block. -
Inside this block, list every flow that your test will call using
<munit:enable-flow-source>.
Example:
<munit:test name="your-main-flow-test">
...
<munit:enable-flow-sources>
<munit:enable-flow-source value="your-utility-flow-name" />
<munit:enable-flow-source value="your-mock-listener-flow" />
</munit:enable-flow-sources>
...
</munit:test>-
Keep Test Flows Together: It’s best practice to define your test and any supporting mock flows within the same MUnit test suite XML file. Referencing flows from different test files can sometimes lead to unexpected behavior.
-
Avoid using
src/main/mulefor Test Flows: Avoid placing test-specific flows (especially those with listeners) in your main application source folder (src/main/mule). If you do, they might be deployed with your application, count as active flows, and potentially increase your subscription costs. If this is unavoidable, configure your build to exclude these test files from the final deployment package.
|
Note
|
Two (or more) configuration elements have been defined with the same global name…
Cause: This error typically happens if you use the Solution: Avoid using |
This is a simpler, more direct approach suitable for basic validation scenarios where the full content of the error object is not required for the test logic.
Original source code: How to test HTTP error in Mule 4 with Munit 2
In this option is important to consider move the flow for HTTP Listener from munitusage.xml in the directory src\main\mule\option-b so the flow and the respective configuration goes to src/test/munit/option-b.
This avoid any invalid usage or even the deploy on Mule Runtime.
You may add to your pom.xml file to ignore the file in the build:
<build>
<plugins>
<plugin>
<!-- INFO: This plugin is not intended to be used like this, but it works. You may need to find another solution and test if it works. -->
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete file="${project.build.outputDirectory}/option-b/munitusage.xml" />
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>This method involves making a direct http:request from within the MUnit test to a live endpoint (running via enable-flow-sources) that is expected to fail. You can then test the outcome in two distinct ways:
-
Expected Mule Error: Configure the
<munit:test>element withexpectedErrorType="HTTP:NOT_FOUND". When thehttp:requestreceives a 404 response, it will throw this Mule error, and because MUnit was expecting it, the test will pass. This validates that the correct error type is generated. -
Success Status Validator: Configure the
http:requestwithin the test to accept a non-2xx status code (e.g., 404) as a "success" response. This prevents the connector from throwing a Mule error, allowing your test to proceed to the<munit:validation>phase where you can assert thatattributes.statusCodeis indeed 404.
sequenceDiagram
participant Test as MUnit Test
participant Listener as Live HTTP Listener (in App)
Test->>Listener: HTTP Request to non-existent path
Listener-->>Test: Returns 404 Response
alt Expecting Mule Error
Test->>Test: HTTP Requester throws HTTP:NOT_FOUND
Test->>Test: Test passes as error was expected
else Using Success Validator
Test->>Test: HTTP Requester treats 404 as success
Test->>Test: Assert attributes.statusCode == 404
end
<mule ...>
<!-- Test Case 1: Expecting a Mule Error -->
<munit:test name="testHTTPNotFound404Error-MuleError" expectedErrorType="HTTP:NOT_FOUND">
<munit:enable-flow-sources>
<munit:enable-flow-source value="munitusage.http-listener-and-error-propagation" />
</munit:enable-flow-sources>
<munit:execution>
<!-- This request to a non-existent path will fail, triggering the expected error. -->
<http:request config-ref="HTTP_Request_configuration" path="/NotExist"/>
</munit:execution>
</munit:test>
<!-- Test Case 2: Validating the Status Code Directly -->
<munit:test name="testHTTPNotFound404Error-HTTPStatusCode">
<munit:enable-flow-sources>
<munit:enable-flow-source value="munitusage.http-listener-and-error-propagation" />
</munit:enable-flow-sources>
<munit:execution>
<http:request config-ref="HTTP_Request_configuration" path="/NotExist">
<!-- This response validator tells the requester not to throw an error for a 404 response. -->
<http:response-validator>
<http:success-status-code-validator values="404" />
</http:response-validator>
</http:request>
</munit:execution>
<munit:validation>
<!-- Since no error was thrown, we can now assert the status code from the response attributes. -->
<munit-tools:assert-equals
actual="#[attributes.statusCode]"
expected="#[404]" />
</munit:validation>
</munit:test>
</mule>-
Simple: Very straightforward to set up for basic use cases, requiring minimal MUnit configuration.
-
Direct: Clearly tests the fundamental behavior of the HTTP listener’s error response mapping without any layers of mocking.
-
Limited Scope: This approach doesn’t effectively test the error handling logic within a flow’s try block. It’s primarily for testing the direct response of a listener or a simple request.
-
No Payload/Attribute Control: You cannot easily test on-error blocks that rely on a specific error payload or custom headers, as the error object generated is minimal or bypassed entirely. For example, a condition like
when="#[error.errorMessage.payload.code == 'E404-USER']"cannot be tested this way. -
Requires Live Endpoint: Relies on having a running flow to test against, which may not always be desirable.
|
Note
|
Test Fails Unexpectedly
Cause: If you are expecting an Solution: Ensure no other mocks are inadvertently catching your request. When using the |
This option is functionally similar to Option A, in that it produces a high-fidelity error object, but it does so through a more complex and less intuitive setup.
Like Option A, this uses mock-when with then-call. However, instead of a simple utility flow, it calls a flow that makes an HTTP request to yet another MUnit flow that has a listener. This second flow contains logic to raise-error with a generic type, which is then caught by its own on-error-continue scope where a response is manually constructed. It achieves the same end result as Option A but with extra, often unnecessary, steps and layers of abstraction.
The key difference is the multi-hop internal call, which adds complexity.
<mule ...>
<!-- The mock calls the first flow, 'impl-option-c-test-suite.trigger-mock-404-http-request' -->
<munit-tools:mock-when processor="http:request">
<munit-tools:then-call flow="impl-option-c-test-suite.trigger-mock-404-http-request"/>
</munit-tools:mock-when>
...
<!-- This flow's only job is to make another HTTP request to the listener below -->
<flow name="impl-option-c-test-suite.trigger-mock-404-http-request">
<http:request config-ref="Test_Error_Status_Codes_HTTP_Request_configuration" path="/mock">
<http:query-params>
<![CDATA[#[{ "expectedStatusCode" : 404 }]]]>
</http:query-params>
</http:request>
</flow>
<!-- This flow listens, raises a generic error, and then manually builds an error response -->
<flow name="impl-option-c-test-suite.http-listener-for-mock-error-responses">
<http:listener config-ref="Test_Error_Status_Codes_HTTP_Listener_config" path="/mock">
<http:error-response statusCode="#[vars.httpStatus default 500]"/>
</http:listener>
<raise-error type="TEST:EXCEPTION"/>
<error-handler>
<on-error-continue type="TEST:EXCEPTION">
<set-variable variableName="httpStatus" value="#[attributes.queryParams.expectedStatusCode]" />
<ee:transform>
<!-- Manually sets the error payload that will be returned -->
</ee:transform>
</on-error-continue>
</error-handler>
</flow>
</mule>-
High Fidelity: Ultimately produces a realistic error object that can be used to test complex error handling logic.
-
Overly Complex: The two-step internal HTTP call is confusing and adds unnecessary overhead and points of failure. Option A achieves the same high-fidelity result in a much more direct and understandable way.
-
Hard to Maintain: The logic is spread across multiple, interdependent flows, making it difficult for another developer to follow the execution path and debug any issues with the test itself.
This approach avoids using live HTTP listeners entirely and instead constructs the required error object directly in DataWeave by instantiating one of the HTTP connector’s internal Java classes.
The munit:set-event or mock-when processor is used to create an error. Its exception attribute is populated with a DataWeave expression that directly invokes the Java constructor for ResponseValidatorTypedException. This is a non-public, internal class used by the HTTP connector when a response validator fails. By calling ::new(), you can programmatically specify the error description, type, and a manually constructed payload message, effectively building the error object from scratch.
sequenceDiagram
participant Test as MUnit Test
participant Flow as Flow Under Test
participant Mock as Mock When (http:request)
Test->>Flow: Execute flow
Flow->>Mock: HTTP Request to external system
Mock->>Mock: then-return with error
Mock->>Mock: DW executes Java constructor for Exception
Mock-->>Flow: Throws a constructed error object
Flow->>Flow: Enters on-error-continue/propagate scope
Test->>Flow: Verify behavior
// This DWL script is called to generate the exception object by directly instantiating a Java class.
java!org::mule::extension::http::api::request::validator::ResponseValidatorTypedException::new(
vars.munitHttpError.description,
vars.munitHttpError.errorType,
java!org::mule::runtime::api::message::Message::of(
java!org::mule::runtime::api::metadata::TypedValue::new(
write(vars.munitHttpError.payload,'application/json',{indent: false}),
java!org::mule::runtime::api::metadata::DataType::JSON_STRING
)
)
)<mule ...>
<flow name="impl-option-d-test-suite.set-error-event-from-file">
<!-- This processor creates the error by executing the DWL script. -->
<munit:set-event>
<munit:error id="HTTP:INTERNAL_SERVER_ERROR" exception="#[${file::option-d/httpError.dwl}]" />
</munit:set-event>
</flow>
</mule><mule ...>
<flow name="impl-option-d-test-suite.set-error-event-from-file">
<!-- This processor creates the error by executing the DWL script. -->
<munit:set-event>
<munit:error
id="HTTP:INTERNAL_SERVER_ERROR"
exception="#[java!org::mule::extension::http::api::request::validator::ResponseValidatorTypedException::new(vars.munitHttpError.description, vars.munitHttpError.errorType, java!org::mule::runtime::api::message::Message::of( java!org::mule::runtime::api::metadata::TypedValue::new( write(vars.munitHttpError.payload,'application/json',{indent:false}), java!org::mule::runtime::api::metadata::DataType::JSON_STRING ) ) )]" />
</munit:set-event>
</flow>
</mule>-
Self-Contained: No need for extra listener or requester flows. The error generation logic is contained entirely within the mock definition and its associated DataWeave script.
-
Fast: Avoids the minor network overhead of an actual local HTTP call, making the test execution marginally faster.
-
Brittle and Unstable: This is the most significant drawback. The test directly relies on internal Java classes (
ResponseValidatorTypedException) of the HTTP connector. These are not part of the public, supported API and could be renamed, moved, or have their constructors changed in any future version of the connector, which would immediately break all tests using this pattern. -
Incorrect Error Type: This method often results in a generic
MULE:UNKNOWNerror type being reported as soon the DataWeave executes and the Java class returns the thrown error. Even if you specify anidlikeHTTP:INTERNAL_SERVER_ERROR. This makes assertions againsterror.errorTypeunreliable. -
Less Realistic: It’s a synthetic simulation of an error, not a genuine one generated by the connector’s own internal logic. This means it may miss subtle behaviors or properties present in a real error object.
When you find the issue below:
org.mule.runtime.api.exception.MuleRuntimeException: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'impl-option-d-test-suite.set-error-event-from-file': Cannot create inner bean '(inner bean)#4a329eca' of type [org.mule.munit.runner.processors.SetEventProcessor] while setting bean property 'messageProcessors' with key [1]; nested exception is Error creating bean with name '(inner bean)#4a329eca': Failed properties: Failed to convert property value of type 'org.mule.munit.common.api.model.UntypedEventError' to required type 'org.mule.munit.common.api.model.UntypedEventError' for property 'error'; class java.lang.String cannot be cast to class java.lang.Throwable (java.lang.String and java.lang.Throwable are in module java.base of loader 'bootstrap'); nested exception is Failed properties: Failed to convert property value of type 'org.mule.munit.common.api.model.UntypedEventError' to required type 'org.mule.munit.common.api.model.UntypedEventError' for property 'error'; class java.lang.String cannot be cast to class java.lang.Throwable (java.lang.String and java.lang.Throwable are in module java.base of loader 'bootstrap')
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'impl-option-d-test-suite.set-error-event-from-file': Cannot create inner bean
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'error_handlingSub_FlowTest': Cannot create inner bean '(inner bean)#2babdabc' of type [org.mule.munit.runner.component.factory.TestProcessorChainFactory_ByteBuddy_org_mule_runtime_core_privileged_processor_chain_MessageProcessorChain] while setting bean property 'processorChains' with key [0]Cause: This runtime error often points to an issue with the version of the MUnit Maven Plugin being used. Older versions (e.g., 3.4.0) had known issues correctly handling the exception attribute in munit:set-event when it was populated by a DataWeave script instantiating an object.
Solution: Ensure your pom.xml is using a recent and stable version of the munit-maven-plugin (e.g. 3.5.0, 3.3.0).
The MUnit test suite test/munit/option-d/docs-mule-set-event-with-error-test-suite.xml tries to validate the same usage of attribute exception to thrown an error based on an example from the official documentation from MuleSoft available on Set an Event with an Error - Testing and Mocking Errors | MuleSoft Documentation
<properties>
<munit.version>3.5.0</munit.version>
</properties>| Feature | Option A (Recommended) | Option B | Option C | Option D |
|---|---|---|---|---|
Error Realism |
Excellent |
Low (for internal logic) |
Excellent |
Fair to Poor |
Control over Error |
Excellent |
Poor |
Excellent |
Good |
Setup Complexity |
Medium |
Low |
High |
Low |
Reusability |
Excellent |
Low |
Fair |
Good (for DWL script) |
Maintainability |
High |
High |
Low |
Medium (risk of breakage) |
Recommendation: Option A
Option A is the clear winner and the recommended best practice for testing HTTP error handling in MUnit. It provides the most realistic simulation of an HTTP error without being overly complex. The error object it produces is identical in structure and metadata to one from a real-world failure, which is paramount for ensuring your error-handling logic is tested accurately and reliably. While it requires a small amount of initial setup for the utility flows, the long-term benefits of reusability, high maintainability, and testing fidelity far outweigh this initial one-time investment, leading to a more robust and professional test suite.



