App Connect

Writing JUnit Error Tests With IBM App Connect Enterprise 12

By Graham Haxby posted Tue July 06, 2021 11:11 AM

  
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 {

		// Define reference to node
		SpyObjectReference nodeReference = new SpyObjectReference().application("MyApplication")
				.messageFlow("UnhandledErrors").node("Account Lookup");

		// Create spy
		NodeSpy nodeSpy = new NodeSpy(nodeReference);

		// Create empty input message assembly
		TestMessageAssembly emptyTestMessageAssembly = new TestMessageAssembly();

		// Assert an exception should be thrown
		TestException thrownException = assertThrows(TestException.class,
				() -> nodeSpy.evaluate(emptyTestMessageAssembly, true, "in"), "Account lookup should throw");
		
		// Look for our error message
		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 {

		// Define reference to node
		SpyObjectReference nodeReference = new SpyObjectReference().application("MyApplication")
				.messageFlow("HandledErrors").node("Account Lookup");

		// Create spy
		NodeSpy nodeSpy = new NodeSpy(nodeReference);

		// Create empty input message assembly
		TestMessageAssembly emptyTestMessageAssembly = new TestMessageAssembly();

		// Evaluate node
		nodeSpy.evaluate(emptyTestMessageAssembly, true, "in");
		
		//Check the error was propagated to the correct terminal
		assertThat(nodeSpy, terminalPropagateCountIs("failure", 1));

		// Retrieve the message assembly from the spy
		TestMessageAssembly failureAssembly = nodeSpy.propagatedMessageAssembly("failure", 1);

		// Verify User Exception and insert
		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.

Permalink