Saturday, June 21, 2008

Writing Integration Tests with the AbstractTestRunner

Writing integration tests around your ESB Services can be a bit of a pain in the head under the current 4.x codebase (5.0 will be fixing many of these issues). A number of resources are managed statically. This is "OK" under normal running conditions, but a major head wrecker when trying to write multiple isolatable tests i.e. tests where you can easily dump/create clean environments for each test, without one test magically screwing with another.

A while ago I created the ESBConfigUtil test utility class (dumb name), which went some way towards making it a little easier to write tests, but was still a bit painfull. The latest addition is the AbstractTestRunner class, which I think is a bit better again. It wraps the ESBConfigUtil class, setting up the test env (ESB deployment, Registry, ESB properties ala the beloved PropertyManager), starting the ESB Controller, running the test and then shutting down the Controller and ensuring that the test env is returned to it's state from before the test run (or at least tries to ;-) ), which hopefully helps to avoid leakage between tests.

The AbstractTestRunner tries to allow you to write Integration level tests more easily. You can define a full ESB configuration and run it through the ESB Controller class, run tests (invoke endpoints etc).

As an example from the InVM transport tests, we have the following ESB configuration named "in-listener-config-01.xml":

<?xml version = "1.0" encoding = "UTF-8"?>
<jbossesb xmlns="http://anonsvn.labs.jboss.com/labs/jbossesb/trunk/product/etc/schemas/xml/jbossesb-1.0.1.xsd">

<services>
<service category="ServiceCat" name="ServiceName"
description="Test Service">
<actions mep="RequestResponse">
<action name="action"
class="org.jboss.soa.esb.mock.MockAction" />
</actions>
</service>
</services>

</jbossesb>

Writing some test code around this, with the configuration deployed in the ESB Controller (quite near to the full running env):

public void test_async() throws Exception {
AbstractTestRunner testRunner = new AbstractTestRunner() {
public void test() throws Exception {
ServiceInvoker invoker =
new ServiceInvoker("ServiceCat", "ServiceName");

Message message = MessageFactory.getInstance().getMessage();

message.getBody().add("Hi there!");
invoker.deliverAsync(message);

sleep(50);
assertTrue(message == MockAction.message);
}
}.setServiceConfig("in-listener-config-01.xml");

testRunner.run();
}

So as you can see, the test method creates an anonymous instance of the AbstractTestRunner class. It implements the "test" method, in which the actual test code is placed. The Service config file is set on the anonymous instance through the setServiceConfig method (the String param version of this method looks up the resource using getClass().getResourceAsStream()). The ESB properties file can be set through the setEsbProperties method (supports the same mechanisms as the setServiceConfig method). Both setServiceConfig and setEsbProperties methods return the instance of the anonymous inner class (i.e. "return this;"), so you can string the config calls together e.g.:

public void test_async() throws Exception {
AbstractTestRunner testRunner = new AbstractTestRunner() {
public void test() throws Exception {
.... test code....
}
}.setEsbProperties("jbossesb-properties.xml")
.setServiceConfig("jboss-esb.xml");


testRunner.run();
}
Of course, you need to call the run() method to run the actual test code. You can do this as above, or you can just string it onto the end of the anonymous inner class:
public void test_async() throws Exception {
AbstractTestRunner testRunner = new AbstractTestRunner() {
public void test() throws Exception {
.... test code....
}
}.setServiceConfig("jboss-esb.xml").run();
}

Calling the run() method:
  1. Parses the Service configuration, creating the Controller instance.
  2. Installs the specified ESB properties (recording the currently installed properties)
  3. Installs and configures the Registry.
  4. Starts the Controller
  5. Calls the "test()" method to run the test code.
  6. Stops the Controller
  7. Uninstalls the Registry.
  8. Resets the ESB properties to their pre-test state.
You may ask why I didn't just implement a base TestCase class. Well... it just ends up getting in the way when you're trying to write your own test code. You end up having to be contious of what you're inheriting, making sure you call super.xxx() at the appropriate times etc. You end up not being able to extend another TestCase base impl (if you need to do that). Basically, as a solution it sucks for many of the same reasons that other inheritance based solutions suck!

Would be great if people could try it in their tests so we can evolve it and fix any issues. I hope it can make integration testing a bit easier in the 4.x codebase.