devxlogo

A Test-Driven Exploration of the Advanced Features of EJB 3.0

A Test-Driven Exploration of the Advanced Features of EJB 3.0

lthough the EJB 3.0 specification has yet to be released, it is already generating much interest in the software development community by both proponents and opponents alike. All recognize the need to find more productive methods for developing software; the debate centers on whether, and to what extent, EJB 3.0 will play a role in this new landscape. Debate notwithstanding, the release of the EJB 3.0 public draft and preliminary support in JBoss mean that now is a great time to explore this influential technology. In fact, reports of the use of EJB 3.0 in production systems are already emerging.

This article is the third in a three-part series exploring EJB 3.0 as defined in the public draft. Each article has introduced particular concepts from the specification and has walked you through the implementation of these techniques using JBoss. The first article introduced EJB 3.0, the philosophy behind it, and illustrated how to use it to develop Enterprise Bean Components. The second article introduced you to developing persistent entities with EJB 3.0. This third article will explore more advanced topics such as transaction management, callbacks, interceptors, and exceptions.

Test-Driven Approach
In this article I’ll extend the online music store application (which was used extensively in the first two articles) following a test-driven approach. What this means is that for each concept that is explored in this article you will first see how to write a unit test verifying the expected behavior of the feature being added. Next you will see how to implement that feature using EJB 3.0.

For an overview of the music store application, please consult the earlier two articles. The storefront is shown in Figure 1.

Figure 1. Music Store: The music store allows products to be browsed, added to a shopping cart, and eventually purchased.

Unit Testing Enterprise Beans
Enterprise Beans in EJB 3.0 are easier to test than EJBs written to prior versions of the specification. This is largely because in the 3.0 specification EJBs are simply POJOs annotated with specific EJB 3.0 annotations. Also, one of the very nice features of the JBoss implementation is that it can be used outside the JBoss Application Server in an embedded configuration. In this article you will see how to unit test EJBs using the JBoss embedded EJB 3.0 container.

To run the JBoss embedded container all you need to do is to download the distribution from JBoss, and place the JAR and configuration files on the classpath of your application. When your application (or unit test in this case) is ready to start the embedded container it uses the EJB3StandaloneBootstrap class to bootstrap the container. Listing 1 shows an abstract JUnit test case that for each unit test starts the embedded container and populates a sample database with known test data:

The data test being used to unit test the music store’s persistent entities is loaded via DbUnit and contains the data shown in Listing 2.

Through the rest of this article you will write unit tests that subclass the above base test case. These test cases can be written assuming that the container and the database are in a known state.

Using this base test case you can now very easily write a unit test for the MusicStoreDAO developed in the last article on EJB 3.0 persistence. For example, you can test the findArtistById method of the DAO by taking the following steps:

  1. Start the embedded container (performed in the setup method of the parent test case).
  2. Load sample data (performed in the setup method of the parent test case).
  3. Retrieve a reference to the MusicStoreDAO stateless session bean through JNDI.
  4. Invoke the findArtistById method with a known artist id.
  5. Lastly, verify that the artist retrieved matches the artist id supplied.

This is very easy with the embedded EJB 3.0 container and the above base test case. The code is shown below:

public class MusicStoreDAOTest extends BaseTestCase{  public void testFindArtistById() throws Exception  {    IMusicStoreDAO dao = (IMusicStoreDAO) getInitialContext().lookup(        IMusicStoreDAO.class.getName());    Artist artist = dao.findArtistById(1);    assertEquals("Norah Jones", artist.getName());  }}

As the above example illustrates, with the JBoss embedded EJB 3.0 container it really is quite easy (and fast) to unit-test EJBs.

Transactions
As you might expect given the heavy use of annotations by EJB 3.0, the transactional requirements of an EJB can be specified through annotations on the bean class. The EJB 3.0 specification defines two annotations: @TransactionManagement and @TransactionAttribute. Both of these annotations are applied at the class level.

The @TransactionManagent annotation defines whether transactions should be managed automatically by the container or manually by the bean developer. The @TransactionAttribute annotation defines the transaction propagation requirements of the bean (required, requires new, supports, etc.). The valid values for each of the annotations are enumerated by constants on the TransactionManagementType and TransactionAttributeType classes respectively.

Also true to form with the design philosophy of EJB 3.0, if either or both of these annotations are omitted the container will supply defaults. If the @TransactionManagement annotation is omitted the container will apply container-managed transactions. If the @TransactionAttribute annotation is omitted the container will assumed the required transaction propagation level.

To test this you will write a unit test for the checkout method of the MusicStore stateless EJB. The checkout method does two things: it saves the user’s order and sends it to the order processor for fulfillment. This is illustrated below:

@Stateless(name = "musicStore")public class MusicStore implements IMusicStore{  @EJB  private IMusicStoreDAO musicStoreDAO;  public void checkOut(IShoppingCart cart)  {    musicStoreDAO.saveMusicOrder(new MusicOrder(cart.listProducts()));    OrderProcessorDAO.getInstance().submitOrder(new MusicOrder(cart.listProducts()));    cart.clear();  }...

Becaue the @TransactionManagement annotation is omitted on the MusicStore class the container will make this method transactional by default. Below is a unit test that causes the order processor to throw an exception (by using EasyMock) and verifies that the saving of the order is properly rolled-back in this situation:

  public void testCheckOut() throws Exception  {    IMusicStore store = (IMusicStore) getInitialContext().lookup(IMusicStore.class.getName());    IShoppingCart cart = new ShoppingCart();    cart.addProductToCart(store.findProductById(1));    cart.addProductToCart(store.findProductById(2));    setupOrderProcessorException();    try    {      store.checkOut(cart);      fail("Exception not thrown");    }    catch (Exception e)    {      ...    }    assertEquals(1, store.listOrders().size());  }

Since by default the container is making the checkout method transactional this unit test passes on the first attempt as illustrated in Figure 2.


Figure 2. Creating Transaction Success: Since by default business methods on enterprise bean components have container-managed transaction applied, the transaction test passes on the first attempt.
 
Figure 3. Transaction Failure: This unit test passed with the default container-managed transaction setting but failed when changed to bean managed. This illustrates the validity of the unit test and the effect of the @TransactionManagement annotation.

However, by apply bean-managed transactions instead you can see this unit test failing, as shown in Figure 3.
@Stateless(name = "musicStore")@TransactionManagement(TransactionManagementType.BEAN)public class MusicStore implements IMusicStore{......}

Callbacks
One of the services that an EJB container provides for an application is lifecycle management of enterprise beans. Lifecycle management means that the container manages a pool of stateless and stateful session beans creating and destroying the beans to provide optimal performance of the application. Because a particular session bean may be removed or passivated (in the case of stateful beans) at any given time, before these events occur the container notifies the affected beans to give them an opportunity to close any open resources. It does so by invoking particular methods on the beans at pre-defined steps during the lifecycle. These methods are referred to as callback methods because the container “calls back” to the EJB.

Prior versions of the EJB specification exposed callback methods via interface methods that all beans were required to implement. These were methods like ejbActivate and ejbPassivate that allowed EJBs to take some actions prior to passivation and after activation. The fact that these methods were defined in the enterprise bean interfaces meant that all enterprise beans had to implement them regardless of whether or not they actually intended to take any particular action in response to the events they represent.

In EJB 3.0 you have much more flexibility about how you choose to tap into the lifecycle of the EJBs you develop. The first choice you have is to define callback methods in the bean class and to annotate these methods with the events you intend them to receive. One example would be to initialize the state of the ShoppingCart stateful session bean after construction. You could express this requirement as a unit test as follows:

public class ShoppingCartTest extends BaseTestCase{  public void testInitialize() throws Exception  {    IShoppingCart cart = (IShoppingCart) getInitialContext().lookup(        IShoppingCart.class.getName());    assertEquals(0, cart.listProducts().size());  }}

And an implementation via a callback method would be:

@Statefulpublic class ShoppingCart implements IShoppingCart{  ...  @PostConstruct  public void initialize()  {    products = new ArrayList();  }  ...}

All callback methods have a signature of “public void ()” and may throw runtime exceptions but not application exceptions. Other callback method annotations include @PreDestroy, @PostActivate, and @PrePassivate. This unit test passes when run as shown in Figure 4.


Figure 4. Callback Method Success: The initialize method has successfully populated the product collections after the bean was constructed by the EJB container.
 
Figure 5. Callback Listener Success: Changing from a callback method to a callback listener doesn’t break the semantics of the testInitialize unit test..

Alternatively, instead of adding callback methods to your enterprise bean class to respond to these lifecycle events you can define a callback listener class instead. This class must be referred to from the enterprise bean class via the @CallbackListener annotation. For example, one implementation of a callback listener for the shopping cart bean would look as follows:
public class ShoppingCartListener{  @PostConstruct  public void initialize(ShoppingCart cart)  {    cart.setProducts(new ArrayList());  }}

Notice that the method signatures for callback methods are slightly different on a listener class then on the bean class. Methods on a listener class include the target object in the method signature and follows the form of “public void(Object).” Dependency injection is not provided to callback listener classes although they may access the enterprise bean’s environment.

And you can attach it to the shopping cart event lifecycle as shown below:

@Stateful@CallbackListener(ShoppingCartListener.class)public class ShoppingCart implements IShoppingCart{  ...}

Re-running the unit test defined above illustrates that both methods produce equivalent results as shown in Figure 5.

Stateful Session Bean Removal
In addition to the callback methods discussed above you can also annotate a business method on a stateful session bean to signal to the container that the bean should be destroyed when the method is invoked. Subsequent attempts to invoke methods on the same EJB should fail as a result. This requirement can be expressed in the unit test below:

public class ShoppingCartTest extends BaseTestCase{  ...  public void testRemove() throws Exception  {    IShoppingCart cart = (IShoppingCart) getInitialContext().lookup(        IShoppingCart.class.getName());    cart.remove();    try    {      cart.listProducts();      fail("Bean should have been destroyed");    }    catch (EJBException e)    {      // Bean successfully removed by container    }  }}

Initially this test fails as illustrated in Figure 6.


Figure 6. EJB Removal Failure: Initially invoking remove on the shopping cart bean has no effect.
 
Figure 7. EJB Removal Success: After adding and annotating a remove method this unit test now passes showing how this method properly caused the bean to be destroyed on the server.

Adding a method annotated with @Remove will cause the above test case to pass as illustrated in Figure 7.
@Stateful@CallbackListener(ShoppingCartListener.class)public class ShoppingCart implements IShoppingCart{  ...  @Remove  public void remove()  {  }}

Interceptors
Most of the features available to enterprise bean components in the 3.0 version of the specification existed in previous versions of the specification but were significantly redesigned. Interceptors, however, are new to EJB 3.0, although not a new concept in the arena of lightweight application development. According to the specification, “an interceptor is a method that intercepts a business method invocation.” In other words, an interceptor allows bean developers to inject functionality before and after the invocation of a business method. An interceptor method can either be in the bean class itself or in a separate class known as an interceptor class. In either case, you can only have one interceptor method in a bean or interceptor class although it is possible for a bean class to have multiple interceptor classes.

The invocation of interceptor methods occurs within the same transaction and security context as the business method being invoked. Additionally, they may throw runtime exceptions or application exceptions defined in the signature of the business method.

Interceptors can be used to handle simple, localized, cross-cutting concerns such as resource initialization or logging. Below is a test case that verifies methods invoked on the music store EJB are properly logged:

public class MusicStoreTest extends BaseTestCase{  ...  public void testLogging() throws Exception  {    // Setup mock logger    MockControl control = MockControl.createControl(ILogger.class);    ILogger mockLogger = (ILogger) control.getMock();    LoggerFactory.setLogger(mockLogger);    // Define expected behavior    mockLogger.log("Before");    mockLogger.log("After");    control.replay();    // Invoke music store method    IMusicStore store = (IMusicStore) getInitialContext().lookup(IMusicStore.class.getName());    store.listGenres();    // Verify results    control.verify();    // Clear mock logger    LoggerFactory.setLogger(null);  }  ...}

Running the above unit test fails initially, as shown in Figure 8, because the original implementation of the music store EJB did not correctly log its invocation.


Figure 8. Logging Interceptor Failure: Because the original implementation of the MusicStore class did not log method invocations as defined in the testLogging unit test, this test fails when executed.
 
Figure 9. Logging Interceptor Method Success: Adding the log intercept causes the testLog unit test to pass.

By adding an interceptor method, as the example code below illustrates, causes this unit test to pass as shown in Figure 9.
@Stateless(name = "musicStore")public class MusicStore implements IMusicStore{  @EJB  private IMusicStoreDAO musicStoreDAO;  @AroundInvoke  public Object log(InvocationContext context) throws Exception  {    Object result = null;    LoggerFactory.getLogger().log("Before");    try    {      result = context.proceed();      LoggerFactory.getLogger().log("After");    }    catch (Exception e)    {      LoggerFactory.getLogger().log("After with exception");      throw e;    }    return result;  }  ...}

Alternatively you could place this functionality in a class of its own, for example a LoggingInterceptor class, and annotate the MusicStore class to use this interceptor class as shown below.

public class LoggingInterceptor{  @AroundInvoke  public Object log(InvocationContext context) throws Exception  {    Object result = null;    System.out.println("Before");    try    {      result = context.proceed();      System.out.println("After");    }    catch (Exception e)    {      System.out.println("After with exception");      throw e;    }    return result;  }}@Stateless(name = "musicStore")@Interceptor(LoggingInterceptor.class)public class MusicStore implements IMusicStore{  ...}

Re-running the above unit test demonstrates that this implementation also satisfies the logging requirement as shown in Figure 10.

Figure 10. Logging Interceptor Class Success: Changing the logging implementation from an interceptor method to an interceptor method continues to allow the testLog unit test to pass.

Interceptors provide a rudimentary tool to address cross-cutting concerns locally within a given group of enterprise bean components. For more then the most trivial case you may want to consider instead the more robust capabilities of a true AOP tool such as AspectJ or JBoss AOP.

Exception Handling
Enterprise beans may throw any checked exceptions declared in their business interface. The EJB 3.0 specification does away with the requirement that methods on a remote interface must throw checked RemoteExceptions. Instead, if any errors are encountered in making the EJB invocation then the container will wrap the underlying exception in an unchecked EJBException object. This includes runtime exceptions thrown by the enterprise bean itself.

For example, assume that the checkout operation on the music store can throw two exceptions. The first indicates that the customer has insufficient funds and this exception, named InsufficientFundsException, is a checked exception and declared in the business interface as shown below.

public interface IMusicStore{  ...  public void checkOut(IShoppingCart cart) throws InsufficientFundsException;  ...}

The second exception is a SystemUnavailableException and is a runtime exception that is thrown, for example, if the music store can’t communicate with the order processor. Because this is an unchecked exception it does not need to be declared in the business interface of the checkout operation.

Below is a unit test that asserts that checked exceptions thrown from enterprise bean components function typically for checked exceptions thrown from POJOs:

public class MusicStoreTest extends BaseTestCase{  ...  public void testCheckoutInsufficientFunds() throws Exception  {    IMusicStore store = (IMusicStore) getInitialContext().lookup(IMusicStore.class.getName());    // Setup exception indicator    MusicStore.insufficientFunds = true;    try    {      store.checkOut(null);      fail("An exception should have been thrown here");    }    catch (InsufficientFundsException e)    {      // Exception was correctly thrown    }  }  ...}

It is simple to control whether the music store throws these simulated exceptions by applying an interceptor to the checkout method as illustrated below:

public class MusicStore implements IMusicStore{  ...  @AroundInvoke  public Object simulateExceptions(InvocationContext context) throws Exception  {    if (insufficientFunds)    {      throw new InsufficientFundsException();    }    if (systemUnavailable)    {      throw new SystemUnavailableException();    }    return context.proceed();  }  ...}

This test passes, as shown in Figure 11, which illustrates that application exceptions declared in the business interface work as expected.

Figure 11. Exception Success: Application exceptions are thrown from enterprise bean components as expected.

The next test case defines the behavior that one would expect from unchecked exceptions thrown from a Java class:

public class MusicStore implements IMusicStore{  ...  public void testCheckoutSystemUnavailable() throws Exception  {    IMusicStore store = (IMusicStore) getInitialContext().lookup(IMusicStore.class.getName());    // Setup exception indicator    MusicStore.systemUnavailable = true;    try    {      store.checkOut(null);      fail("An exception should have been thrown here");    }    catch (SystemUnavailableException e)    {      // Exception was correctly thrown    }  }  ...}

This test initially fails with the exception shown in Figure 12.


Figure 12. Exception Failure: Runtime exceptions are automatically wrapped with an EJBException by the EJB container.
 
Figure 13. Unchecked Exception Success: By annotating the unchecked SystemUnavailableException with the @ApplicationException annotation it is now treated as an application exception and not wrapped with an EJBException.

This exception is caused because non-application exceptions, i.e. exceptions not declared in the business interface, are wrapped by the EJB container in an EJBException. In order to declare that an arbitrary exception, such as the SystemUnavailableException in our example, is to be treated as an application exception and not wrapped, the exception should be annotated with the @ApplicationException annotation. Adding this annotation, as shown below, allows the unit test to pass as expected (see Figure 13).
@ApplicationExceptionpublic class SystemUnavailableException extends RuntimeException{}

The proposed EJB 3.0 specification offers many productivity enhancements for enterprise Java development. Its emphasis on providing enterprise services to POJOs facilitates, among other things, the unit testing of enterprise java beans. The JBoss embedded EJB 3.0 implementation is particularly useful in this regard by providing a lightweight EJB 3.0 container for hosting EJB 3.0 outside of a full-fledged J2EE application server. In this article you learned how to apply a test-driven approach to unit testing many facets of EJB development including transactions, callbacks, interceptors, and exception handling.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist