One of the huge benefits of Unit Testing is the ability to rapidly test error scenarios, many of which are otherwise hard or impossible to test. So it's worth considering how we might approach these different scenarios when testing message flows and nodes. We'll concentrate on individual node testing, but the same ideas apply for larger test units like groups of nodes or whole end to end flow testing.
The types of error fall into two categories, handled exceptions or errors, and unhandled ones.
Unhandled Node Errors
When an exception occurs in a message flow and failure terminals aren't connected the exception is thrown back up the flow until it's handled elsewhere, or it hits the input node. At test time when invoking a single node (using a node test double like a NodeSpy) a TestException will be thrown.
Upstream Error Handling
For cases where the error is handled upstream you probably want to get the exception in the test and assert the correct message in the exception. This flow doesn't use the failure terminal of the Account Lookup node but instead has a try catch upstream:

Example JUnit test that shows asserting an exception is thrown and verifying the error text "MyError" is present:
@Test
public void generatedExceptionContainsExpectedText() throws TestException {
SpyObjectReference nodeReference = new SpyObjectReference().application("MyApplication")
.messageFlow("UnhandledErrors").node("Account Lookup");
NodeSpy nodeSpy = new NodeSpy(nodeReference);
TestMessageAssembly emptyTestMessageAssembly = new TestMessageAssembly();
TestException thrownException = assertThrows(TestException.class,
() -> nodeSpy.evaluate(emptyTestMessageAssembly, true, "in"), "Account lookup should throw");
assertThat(thrownException, causedBy(containsMessageText("MyError")));
}
No Error Handling
If there is no error handling of this particular error, then a failing unit test likely indicates you're missing error handling and you've identified an issue that needs resolving.
Removing assertThrows from the above example causes the test to fail with details of what went wrong:
TEST RESULTS:
test.MyApplication:
generatedExceptionContainsExpectedText():FAILED
TestException(BIP2230) BIP2230E: Error detected whilst processing a message in node 'UnhandledErrors.Account Lookup'. {'Caught exception and rethrowing'} [ TestException(BIP2488) BIP2488E: (.Transformation_Map_Compute.Main, 4.3) Error detected whilst executing the SQL statement 'THROW EXCEPTION VALUES('MyError', 1234);'. {'Error detected, rethrowing'} [ TestException(BIP2951) BIP2951I: Event generated by user code. Additional information : 'MyError' '1234' 'UnhandledErrors.Account Lookup' '{3}' '{4}' '{5}' '{6}' '{7}' '{8}' '{9}' {'User generated exception'} ] ]
com.ibm.integration.test.v1.NodeSpy._evaluate(Native Method)
com.ibm.integration.test.v1.NodeSpy.evaluate(NodeSpy.java:336)
test.MyApplication.generatedExceptionContainsExpectedText(MyApplication.java:45)
Handled errors
Handled errors are cases where the node's failure or catch terminal is wired up and the error is either handled or further retry logic etc is designed into the message flow. In these cases, the message assembly propagated to the failure or catch terminal will contain an exception list to drive the downstream flow logic. The flow has now been changed so the error is handled downstream and the test will not see an exception thrown:
We can modify the test to check the terminal the message assembly was propagated to and verify the contents:
@Test
public void verifyExceptionListOnError() throws TestException {
SpyObjectReference nodeReference = new SpyObjectReference().application("MyApplication")
.messageFlow("HandledErrors").node("Account Lookup");
NodeSpy nodeSpy = new NodeSpy(nodeReference);
TestMessageAssembly emptyTestMessageAssembly = new TestMessageAssembly();
nodeSpy.evaluate(emptyTestMessageAssembly, true, "in");
assertThat(nodeSpy, terminalPropagateCountIs("failure", 1));
TestMessageAssembly failureAssembly = nodeSpy.propagatedMessageAssembly("failure", 1);
assertThat(failureAssembly, hasLocalExceptionListTreeElement("RecoverableException.RecoverableException.UserException.Insert[1].Text").equals("MyError"));
}
The earlier examples all use an empty message assembly to force an error looking up an account, but you can use the above approaches to rapidly test different shape input messages. It is best practice to test for failure and success and in the case of failure, that you get the expected failure to make sure you are successfully driving the behaviours you are trying to test.
Injecting errors for whole flow or multiple node tests
Another type of test double called a NodeStub can also be very powerful to generate the harder to create errors, by training the node to behave differently at test time; an example being a failed HTTP request.