Skip to content

andersoal/mule-example-munit-http-error

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MUnit HTTP Error Handling: A Comprehensive Guide

1. Introduction

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.errorType will not complain with the expected error.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.

2. General Considerations

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 ⚠️ The flow mentioned in the mock then call option doesn’t need to be enabled in the MUnit test case.

ℹ️ 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]:

2.1. Dynamic Port

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.

3. Reference Guide: Deep Dive into Options

This section provides a detailed breakdown of each of the four testing strategies.

3.1. Option A: Realistic Mocking with a Utility HTTP Listener

This is the recommended approach for its balance of realism, reusability, and fine-grained control over the mocked error.

3.1.1. Approach

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.

3.1.2. Diagrams

Sequence Diagram for Option A
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
option a mermaid sequence diagram

3.1.3. Code Analysis

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.

impl-test-suite.xml
<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 -&gt; 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 -&gt; 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 -&gt; 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>
option a.implementation
option a.munit util mock http
option a.munit listener

3.1.4. Pros and Cons

Pros
  • High Fidelity: Generates a true error.errorMessage object, 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 munitHttp and munitBody variable 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.

Cons
  • 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.

3.1.5. Common Pitfalls & Troubleshooting

Note
Error: Referenced component '…​' must be one of stereotypes [MULE:FLOW, MULE:SUB_FLOW]
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.

3.1.6. Cause

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.

3.1.7. Solution

You need to explicitly tell MUnit to start all required flows for your test.

  1. In your test case, add the <munit:enable-flow-sources> block.

  2. 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>

3.1.8. Other Recommendations

  • 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/mule for 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 <import> tag in your MUnit XML file. While it seems like a logical way to include utility flows, it’s a trap.

Solution: Avoid using <import> in MUnit files. You can enable them as needed using <munit:enable-flow-sources>.

3.1.9. Screenshot Placeholders

3.2. Option B: Direct Error and Status Code Validation

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.

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>

3.2.1. Approach

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 with expectedErrorType="HTTP:NOT_FOUND". When the http:request receives 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:request within 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 that attributes.statusCode is indeed 404.

3.2.2. Diagram

Sequence Diagram for Option B
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

3.2.3. Code Analysis

testHTTPNotFound404Error.xml
<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>

3.2.4. Pros and Cons

Pros
  • 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.

Cons
  • 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.

3.2.5. Common Pitfalls & Troubleshooting

Note
Test Fails Unexpectedly

Cause: If you are expecting an HTTP:NOT_FOUND error but the test fails, it could be because another error is being thrown first, or a response validator is unintentionally interfering with the outcome.

Solution: Ensure no other mocks are inadvertently catching your request. When using the success-status-code-validator, it is critical that you remove the expectedErrorType attribute from the <munit:test> element, as you are explicitly telling MUnit not to expect an error.

3.3. Option C: Complex Internal HTTP Call

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.

3.3.1. Approach

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.

3.3.2. Code Analysis

The key difference is the multi-hop internal call, which adds complexity.

impl-option-c-test-suite.xml
<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>

3.3.3. Pros and Cons

Pros
  • High Fidelity: Ultimately produces a realistic error object that can be used to test complex error handling logic.

Cons
  • 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.

3.3.4. Screenshot Placeholders

3.4. Option D: Manual Java Exception Creation

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.

3.4.1. Approach

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.

⚠️

3.4.2. Diagram

Sequence Diagram for Option D
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

3.4.3. Code Analysis

httpErrorDynamic.dwl
// 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
        )
    )
)
impl-option-d-test-suite.xml with referenced file code
<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>
impl-option-d-test-suite.xml with inline code
<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>

3.4.4. Pros and Cons

Pros
  • 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.

Cons
  • 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:UNKNOWN error type being reported as soon the DataWeave executes and the Java class returns the thrown error. Even if you specify an id like HTTP:INTERNAL_SERVER_ERROR. This makes assertions against error.errorType unreliable.

  • 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.

3.4.5. Common Pitfalls & Troubleshooting

class java.lang.String cannot be cast to class java.lang.Throwable

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>

4. Comparative Analysis & Recommendation

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.


About

MuleSoft Example for MUnit test case that returns proper Mule error (error.errorType - i.e. HTTP:NOT_FOUND) for attributes (error.errorMessage.attributes - i.e., 404) and proper HTTP body (error.errorMessage.payload).

Topics

Resources

Stars

Watchers

Forks

Contributors