by Phil Zoio

A lightweight nonintrusive EJB testing framework

news
Nov 15, 200413 mins

Bring your EJB 2.0 applications in line with testing best practices

Enterprise JavaBeans (EJB) has become increasingly less popular among J2EE developers in the last couple of years. Many factors contribute to this poor reputation, but high on the list is the fact that EJB components are inherently difficult to test.

The EJB 3.0 Expert Group recognizes that testing EJB applications proves difficult, and the upcoming specification is expected to address the issue. However, at the time of this writing, EJB 3.0 is still in early draft review (see Resources). The specification’s finalization and widespread implementation by vendors is still many months away.

In a typical current EJB application, business logic hides behind session-bean facades. For performance reasons, a large portion of the code running on the EJB container is exposed only via EJB local interfaces, which are inaccessible to a suite of JUnit tests running on a separate JVM. In the case of container-managed persistence (CMP) entity beans, much of this code also relies on container-provided services, making it virtually impossible to run outside the EJB container. For example, the EJB CMP bean classes that the application developer writes are themselves abstract classes, the concrete implementations of which are provided by the container. In short, creating a suite of comprehensive unit tests for an EJB-based application creates a real headache for any proponent of test-driven development (TDD).

Two approaches tackle this problem. First is the use of mock objects, which involves a dummy implementation of the EJB API, that can run outside the EJB container. An example is MockEJB. The second approach involves the use of an in-container testing framework such as Cactus, from the Apache Jakarta Project. Here, the idea is to deploy the test code onto the container and run the tests in the application’s real execution environment.

In this article, I present a framework that follows the in-container test approach, but has the advantage of being simple to understand and use. It involves the following:

  • An additional stateless session bean
  • An interface that must be implemented for individual tests
  • An implementation of this interface for each test to be run
  • A JUnit base class to allow integration with the suite of tests to be executed on the client
  • A small modification to the build environment

The framework

Before we discuss how the framework works, let’s first consider our requirements for an EJB testing mechanism:

  • The mechanism should be nonintrusive. That is, we should be able to deploy it with minimum interference from the application’s development build environment.
  • Ideally, there should be minimal dependencies on additional external libraries. We’d rather not have to add jar files to the application server’s classpath or to the EJB jar file’s lib directory to enable the framework.
  • The test framework should integrate well with the JUnit tests that run on the developer’s client machine.
  • Writing the tests themselves should be easy.

Let’s now explore how our framework is implemented.

The test interface

A key component in the framework is the interface ContainerTest, the test interface for which any implementation class constitutes a test case. The methods in the test class execute on the EJB container, and are invoked by the UnitTester EJB, which I discuss shortly.

ContainerTest defines the following methods:

 public void setUp(Object[] args) throws Exception;
public boolean doTransaction();
public void runTest(Map resultMap) throws Exception;
public void afterTransaction(Map resultMap) throws Exception;
public void tearDown(boolean succeeded) throws Exception;

The test interface models a JUnit test’s lifecycle, with setUp() and tearDown() methods. These methods can be used to set up state before running the tests and revert state afterwards. Note that arguments can be supplied to the setUp() method from the client in the form of an Object array, although this is optional.

The ContainerTest implementations should observe a contract established by the implementation of the bean class UnitTesterEJB. Interested readers can peruse the source code in the class UnitTesterEJB for a better understanding of exactly how the UnitTester EJB is implemented. The contract is designed to make testing as simple and logical as possible given a typical EJB application’s transactional requirements.

The ContainerTest interface defines the following methods:

  • The setUp() and tearDown() methods run in their own separate transactions.
  • If setUp() fails, the transaction surrounding setUp() rolls back and none of the subsequent ContainerTest methods execute.
  • The test class uses doTransaction() to control whether transactions are initiated in the invoking EJB or within the test itself. If doTransaction() returns true, then runTest() will execute within an existing transaction initiated by the EJB component. Since the transaction will not have committed when runTest() returns, afterTransaction() can be used to make additional assertions on post-transaction system state.
  • afterTransaction() will not be called if doTransaction() returns false, or if the transaction surrounding runTest() does not commit successfully. Note also that afterTransaction() is not run within a transaction.
  • tearDown() can be used to reset system state. The boolean succeeded parameter indicates whether the transaction surrounding runTest() committed successfully; this can be used to help determine which operations need to execute to restore the system to its pretest state.

The use of these methods will make more sense when I cover an example later in the article, but first we need to look at the framework’s other components.

The remote interface

As I mentioned earlier, the only additional server component required—apart from the tests themselves—is a stateless session bean (SSB). As EJB developers know, this is one of the simplest components to deploy, since it involves little overhead in terms of additional deployment descriptor entries and resource configurations. The unit-test SSB’s job is to simply invoke the specified test, do some generic exception-handling, and return the test execution’s result.

Our SSB has only one business method, defined in the UnitTester remote interface:

 public interface UnitTester extends EJBObject
{
    public UnitTestResult test(String className, Object[] args) throws RemoteException;
}

The mandatory argument supplied to the test() method is the fully qualified name of the class to be tested, which obviously needs to be an instance of ContainerTest. The additional arguments supplied as an Object array are passed to the ContainerTest‘s setUp() method.

If executed, the bean’s test() method will always return a UnitTestResult instance, unless the test class cannot be instantiated or is of the incorrect type.

The UnitTestResult encapsulates the result of the test run. It includes the following:

  • A readable message indicating whether the test succeeded, and, if the test failed, the stage of failure
  • The execution duration of the ContainerTest‘s runTest() method, plus any time taken to start and end the surrounding transaction
  • Optional return values (or any other arbitrary data) resulting from the test execution in the form of a java.util.Map instance
  • Any exception arising from the ContainerTest runTest() or afterTransaction() method calls
  • Any exception arising from the ContainerTest setUp() or tearDown() method calls

UnitTestResult‘s key purpose is to allow easy integration with the JUnit client test suite, as you will see in the next section.

Integration with JUnit

Our framework does not depend on JUnit on the server. However, with JUnit being the unit-testing tool of choice among most J2EE developers, integration with JUnit on the client is an obvious requirement.

In our framework, we integrate JUnit on the client using the class BaseEJBTest, a subclass of junit.framework.TestCase. BaseEJBTest uses the setUp() method to obtain a remote reference to the UnitTester EJB. A Hashtable containing the JNDI (Java Naming and Directory Interface) properties needed for this operation is obtained using the abstract method getEnvironment(). A BaseEJBTest subclass must implement this method. In the tearDown() method, BaseEJBTest releases the UnitTester bean by calling the UnitTester remove() method.

BaseEJBTest also provides a protected convenience method, doTestRun(String className, Object[] args), which subclasses can use to run individual tests. This method simply invokes the UnitTester EJB’s test() method and inspects the result, determining whether it should be treated as a failure or an error, and propagates the result to the JUnit framework. In most cases, only one line of code per client JUnit test method is required for each in-container test case.

The screenshot below shows what this integration looks like in the Eclipse IDE.

A step-by-step example

To demonstrate the framework, I have coded an absurdly simple EJB application, which consists of a single entity bean with a local-only interface, unimaginatively called EntityLocal, and a session bean with a remote interface called SessionRemote.

The entity bean has only two properties, a name and an integer ID, which serves as the primary key. The session bean exposes create/read/update/delete (CRUD) operations for the entity.

Without an in-container testing framework, we would have no easy way of testing the entity bean. Using our framework, we can implement our tests in three steps:

  • For deployment, package the UnitTester bean with the application’s entity and session beans. The article source code includes the Java source, deployment descriptors, and a build script for deployment on JBoss 3.2. More detailed instructions for JBoss 3.2 are also available in the readme file in the source folder’s build directory. With a little effort, it should be fairly straightforward to build for other EJB 2.0 containers.
  • Write the ContainerTest implementations for the in-container functionality to be tested.
  • Write a BaseEJBTest subclass for each logical set of tests, with each testXXX() method separately calling doTestRun() to invoke one of the server-side tests.

I spend some time in the next sections talking about the latter two steps, since these are specific to the testing framework.

Write a container test

The example that follows shows how we can test our entity bean’s update operations. The skeleton for our implementation class shows the lifecycle of our test in operation.

 public class EntityUpdateTest extends AbstractContainerTest
{
   public void setUp(Object[] args) throws Exception
   {
      //Use the LocalHome.create method to create a new persistent entity   
   }
   
   
   public void runTest(Map resultMap) throws Exception
   {
      //Set the name of the entity to something arbitrary
   }
   public void afterTransaction(Map resultMap) throws Exception
   {
      //Use JDBC to check that the entity has changed as expected
   }
   public void tearDown(boolean succeeded) throws Exception
   {
      //Remove the entity
   }
}

We use setUp() to create the persistent entity that will be the tests’ target. We then update the entity in the runTest() method. Since runTest() runs within a transaction, the full effect of the code executed in runTest() is not visible when this method returns. Instead, we use JDBC (Java Database Connectivity) in the afterTransaction() method to verify that the update has occurred correctly. Finally, we use tearDown() to delete the entity.

We won’t examine setUp() and tearDown() any further, since these involve simple operations familiar to any EJB developer. However, a closer look at runTest() and afterTransaction() is worthwhile, since these methods show the test framework in action:

 public void runTest(Map resultMap) throws Exception
{
   Integer entityId = new Integer(1);
   EntityLocalHome home = (EntityLocalHome) lookup("ejb/EntityLocalHome");
   EntityLocal local = home.findByPrimaryKey(entityId);
   local.setName("Some new name");
   assertEquals("Some new name", local.getName());
}

After changing the entity’s name, we use the assertEquals() method to verify that the name has changed correctly in the in-memory bean instance. assertEquals() is one of the convenience methods found in AbstractContainerTest, which the framework provides to facilitate writing ContainerTest implementations. If the assertion fails, an AssertionFailedException is thrown and propagated via the invoking UnitTester EJB to the client.

AbstractContainerTest is a useful base class because it provides dummy implementations for most of the ContainerTest public methods, as well as convenience methods for looking up JNDI references, and methods to facilitate making assertions. We see more examples of these in the afterTransaction() method, which inspects the database to make sure the change has persisted correctly:

 public void afterTransaction(Map resultMap) throws Exception
{
   Connection con = null;
   PreparedStatement ps = null;
   ResultSet result = null;
   try
   {
      con = getConnection("DefaultDS");
      ps = 
      con.prepareStatement("SELECT NAME FROM Entity WHERE ID = ?");
      ps.setInt(1, 1);
      result = ps.executeQuery();
      assertTrue(result.next(), null);
      assertEquals("Some new name", result.getString(1));
   }
   finally
   {
      //Close the JDBC connection, statement and result set 
      //implementation not shown
   }
   
}

Call the tests

Of course the tests are not of much use unless we can call them. As discussed earlier, we are able to do this easily by writing a subclass of BaseEJBTest, which we call OurEJBClientTest:

 public class OurEJBClientTest extends  BaseEJBTest
{
   public static void main(String[] args)
   {
      junit.textui.TestRunner.run(OurEJBClientTest.class);
   }
   
   protected Hashtable getEnvironment()
   {
      Hashtable h = new Hashtable();
      h.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
      h.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces");
      h.put("java.naming.provider.url", "localhost");
      return h;
   }
   public void testAddingEntity()
   {
      doTestRun(EntityAddingTest.class.getName(), null);
   }
   
   public void testUpdatingEntity()
   {
      doTestRun(EntityUpdateTest.class.getName(), null);
   }
}

Note that each test case is simply used to trigger invocation of methods in one of the ContainerTest classes. The ContainerTest example described in the previous section is called from the method testUpdatingEntity(). As mentioned earlier, we can easily supply arguments for or make assertions on return values from ContainerTest method invocations if necessary.

Discussion

As you can see, the framework is easy to use. Once the basic infrastructure is in place, new tests can be added simply by providing new ContainerTest implementations and using the mechanism provided to hook these into the JUnit-based client test suite. The example provided may not be realistic, or even appropriate, but it does show how the mechanisms can be made to work.

However, you should be aware of a few items. Since the framework only supports tests run on the container, a relatively long develop/deploy/test cycle remains for each piece of code written. Since the EJB server and database both need to run for the tests to execute successfully, the automated test environment is more complex than an environment where all tests are run in isolation, without any external dependencies. On the other hand, I argue that some automated integration unit-testing—with a running application server—should be considered essential for any nontrivial TDD-based J2EE project, regardless of whether EJB is used. The EJB test framework could easily be put to work on the back of this integration unit-testing infrastructure.

Finally, while the modifications to the production build environment are minor, they do have an impact, a topic this article does not address. Ideally, the relevant deployment descriptors and/or build scripts should have some preprocessing logic to ensure that the test code, and the UnitTester EJB in particular, is not included when the application is packaged for production.

Conclusion

The inherent difficulty in testing EJB-based applications has been a major contributing factor to the backlash against EJB in the last couple of years. As Rod Johnson puts it in his excellent book Expert One-on-One J2EE Development Without EJB:

“Not many books so far face up to this, but EJB is not compatible with TDD. If you want to practice TDD—and you should!—you will find yourself trying to eliminate EJB usage or, at least, minimize it.”

It is hard to disagree with Johnson’s view that, for new projects, EJB in its current form should be jettisoned in favor of lightweight containers such as Spring. However, this is not always a realistic alternative, especially for many existing projects. The framework presented in this article can be used for TDD-based J2EE applications in which EJB still plays a major role.

Phil Zoio is an independent Java and J2EE consultant. His computing interests include open source software, distributed computing, relational databases, and plug-in development using Eclipse.