■ 11.3—Test a session bean method in a real container
■ 11.6—Test a BMP entity bean
■ B.1—Too simple to break
11.2 Test a legacy session bean
◆ Problem
You need to test a legacy session bean, which appears to require being tested in a container. You are hoping that there is a better way.
◆ Background
You have inherited session beans that you are not allowed to change, for whatever reason. As a result, you need to test them in a container, but doing so is costly in at least two ways:
■ The tests are slower to execute, costing you time every time you execute the tests.
■ The tests are more complicated to write, adding effort to writing the tests.
Nevertheless, if the session bean in question embeds the business logic directly inside it, you have no choice but to test it within a container. The good news is that it need not be a real container, saving you some time, if not effort.
◆ Recipe
If your session bean depends on other objects it obtains through a JNDI lookup—
and that is the most likely case—then your best option is to use the MockEJB (www.mockejb.org) framework.7 This allows you to substitute fake implementa- tions of those collaborators at runtime, which further isolates the session bean’s behavior from the behavior of its collaborators. This way you can test just the ses- sion bean. You will test its collaborators in isolation as well, but not right now.
(One thing at a time.) Consider the common example of a session bean that con-
7 There were some major changes between version 0.4 (August 2003) and 0.5 (December 2003) of Mock- EJB. We use version 0.5 in this book.
388 CHAPTER 11
Testing Enterprise JavaBeans
tains some business logic and uses an entity bean for object persistence. Say that we have inherited an implementation of a session bean to manage a shopper’s shopcart, shown in listing 11.4. Here, LegacyShopcart is an entity bean represent- ing a single shopcart.
package junit.cookbook.coffee.model.ejb;
import java.util.*;
import javax.ejb.CreateException;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
import junit.cookbook.coffee.model.CoffeeQuantity;
public class LegacyShopcartOperationsBean implements javax.ejb.SessionBean {
private javax.ejb.SessionContext mySessionCtx;
private LegacyShopcart shopcart;
public javax.ejb.SessionContext getSessionContext() { return mySessionCtx;
}
public void setSessionContext(javax.ejb.SessionContext ctx) { mySessionCtx = ctx;
}
public void addToShopcart(Vector requestedCoffeeQuantities) { for (Iterator i = requestedCoffeeQuantities.iterator();
i.hasNext();
) {
CoffeeQuantity each = (CoffeeQuantity) i.next();
String eachCoffeeName = each.getCoffeeName();
int currentQuantity;
if (shopcart.containsCoffeeNamed(eachCoffeeName)) {
currentQuantity = shopcart.getQuantity(eachCoffeeName);
} else {
currentQuantity = 0;
}
shopcart.setQuantity(
eachCoffeeName,
currentQuantity + each.getAmountInKilograms());
} }
public void ejbCreate() throws javax.ejb.CreateException { try {
Listing 11.4 A session bean using an entity bean
389 Test a legacy session bean
Context context = new InitialContext();
Object homeAsObject = context.lookup("ejb/legacy/Shopcart");
LegacyShopcartHome home =
(LegacyShopcartHome) PortableRemoteObject.narrow(
homeAsObject,
LegacyShopcartHome.class);
shopcart = home.findByUserName("jbrains");
}
catch (NamingException e) { throw new CreateException(
"Naming exception: " + e.getMessage());
} }
public void ejbActivate() { }
public void ejbPassivate() { }
public void ejbRemove() { shopcart = null;
} }
Unfortunately for us, this is legacy code, which we define as “code without tests.”8 Adding to our bad fortune, we have been instructed to test this code and not refac- tor it, because of time constraints and project pressures. Here is our approach:
1 Use MockEJB and the Self-Shunt Pattern9 to “fake out” looking up a shop- cart using LegacyShopcartHome. Without this, we would need test data and a live database to execute this test. No thanks!
2 Use EasyMock to create a mock LegacyShopcart. Without this, the test would depend on the correctness of the LegacyShopcartEJB, which we plan to test separately, anyway. One thing at a time.
3 Use MockEJB to deploy our fake LegacyShopcartHome, which will return our mock LegacyShopcart when our session bean invokes findByUserName().
8 Some people find this definition amusing, but others have commented how apt it is. You decide.
Apparently Alan Francis also defines legacy code this way, and said so at Agile Scotland 2003. Strictly speaking, we wrote these words here before he spoke them there, but I suppose we can call it a draw.
(See www.scottishdevelopers.com/modules/news/article.php?storyid=11)
9 www.objectmentor.com/resources/articles/SelfShunPtrn.pdf, written by Michael Feathers.
390 CHAPTER 11
Testing Enterprise JavaBeans
With these Test Objects in place, we can write our test. First, let us look at the code that deploys all the objects we are not testing, shown in listing 11.5.
package junit.cookbook.coffee.model.ejb.test;
import java.util.*;
import javax.ejb.*;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
import junit.cookbook.coffee.model.CoffeeQuantity;
import junit.cookbook.coffee.model.ejb.*;
import junit.framework.TestCase;
import org.easymock.MockControl;
import org.mockejb.*;
import org.mockejb.jndi.*;
import org.mockejb.jndi.MockContext;
public class AddToShopcartTest extends TestCase
implements LegacyShopcartHome {
private static final String SESSION_BEAN_JNDI_NAME = "ejb/legacy/ShopcartOperations";
private static final String ENTITY_BEAN_JNDI_NAME = "ejb/legacy/Shopcart";
private LegacyShopcart mockShopcart;
private MockControl shopcartControl;
private MockContainer mockContainer;
protected void setUp() throws Exception { MockContextFactory.setAsInitial();
Context context = new InitialContext();
context.bind(ENTITY_BEAN_JNDI_NAME, this);
shopcartControl =
MockControl.createNiceControl(LegacyShopcart.class);
mockShopcart = (LegacyShopcart) shopcartControl.getMock();
mockContainer = new MockContainer(context);
deployLegacyShopcartOperationsEjb(mockContainer);
}
private void deployLegacyShopcartOperationsEjb(
MockContainer mockContainer) throws NamingException {
Listing 11.5 Deploying some objects with MockEJB
Mock-deploy test case as entity bean home
Use EasyMock to generate mock entity bean Mock-deploy
session bean
TE AM FL Y
Team-Fly®
391 Test a legacy session bean
SessionBeanDescriptor shopcartOperationsBeanDescriptor = new SessionBeanDescriptor(
SESSION_BEAN_JNDI_NAME,
LegacyShopcartOperationsHome.class, LegacyShopcartOperations.class, LegacyShopcartOperationsBean.class);
mockContainer.deploy(shopcartOperationsBeanDescriptor);
}
public LegacyShopcart findByUserName(String userName) { return mockShopcart;
}
public void remove(Object object)
throws RemoveException, EJBException { // Intentionally do nothing
} }
We use the Self-Shunt pattern here so that we can easily substitute our mock Leg- acyShopcart for the actual LegacyShopcart entity bean that the EJB container will return in production. You might be tempted to write a small, in-memory version of LegacyShopcartHome, complete with a Map of user names to shopcart objects, but for this test there is no need. We do not care where the shopcart entity bean comes from, or even if it is a real entity bean! Instead, we are concerned with test- ing the session bean in isolation. That is the motivation for using MockEJB. Now that we have all the Test Objects in place, let us look at the test, which we add to class AddToShopcartTest:
public void testEmptyShopcart() throws Exception { final String coffeeName = "Sumatra";
mockShopcart.containsCoffeeNamed(coffeeName);
shopcartControl.setReturnValue(false);
mockShopcart.setQuantity(coffeeName, 1);
shopcartControl.setVoidCallable();
shopcartControl.replay();
LegacyShopcartOperationsHome home = lookupShopcartOperationsHome();
Vector requestedQuantities = new Vector() { {
add(new CoffeeQuantity(1, coffeeName));
} };
Mock- deploy session bean
“Self- Shunt”
methods
392 CHAPTER 11
Testing Enterprise JavaBeans
home.create().addToShopcart(requestedQuantities);
shopcartControl.verify();
}
The majority of this test is concerned with setting expectations on the mock Lega- cyShopcart entity bean. Because we do not have access to what the original pro- grammer was thinking at the time she wrote this code, we are forced to treat the code as the specification on which to base our tests. If you have access to a docu- mented specification and can verify that that specification still makes sense for the current needs of the project, then use that to write your tests. Here, we are trying to add a single coffee product to an empty shopcart, so we expect LegacyShopcart- OperationsBean to do the following:
■ Ask the shopcart if it contains any Sumatra coffee.
■ Set the current quantity of Sumatra to be 1 kg, since there was no Sumatra in the shopcart.
Because the shopcart is empty, we need to tell the mock shopcart to return false when asked whether it contains any Sumatra coffee, so we invoke setReturn- Value(false) to do that. This is the way we simulate an empty shopcart, rather than duplicate an empty shopcart using an in-memory implementation of a shop- cart (probably just a wrapper around a Map of product names to quantities). It is a bad idea to duplicate the essential logic of a shopcart—once in the test and once in the production entity bean. We would use the production entity bean if we could do so without dragging that big ugly EJB container with it.
As with most mock object approaches, we invoke mockShopcart.verify() at the end of our test to verify that the expectations we set at the beginning of the test are met by the production code. In this case, they are, and the test passes! First, notice that we were able to test the session bean without changing any of its code, but also notice the amount of effort that went into mocking the behavior of all the session bean’s collaborators: its home interface, an entity bean, and the EJB con- tainer. If we were able to separate the business logic from the session bean, we would at least be able to eliminate the need to simulate the EJB container. Com- pare this to recipe 11.1 to see how avoiding the EJB container (and JNDI service) can simplify testing as well as improve the flexibility of your design.
◆ Discussion
If your session bean does not interact with the application server’s services at all, then there is a more straightforward approach. In this case, just instantiate the EJB implementation class in your test and invoke methods on it. With this approach,
393 Test a legacy session bean
you need to simulate the container to a certain extent, invoking the EJB lifecycle methods to ensure that each does what it should. The good news is that, for exam- ple, you can test your bean’s passivation behavior by simply invoking ejbCreate() followed by ejbPassivate(). There is no need for the complex test environment setup of starting a container. You merely simulate that small part of the con- tainer’s behavior germane to the current test. We recommend using the EJB life- cycle diagrams in the EJB specification as a guideline in order to determine which lifecycle methods to invoke for a given test. When you take this approach, you are treating the EJB implementation class just like any other Java class, so you can apply whatever testing techniques are appropriate. You can forget for the moment that you are testing an EJB, and that just makes the tests easier.
If you are able to test your session bean using MockEJB, then keep in mind that your job is not quite finished. In using MockEJB you are mocking up your session bean’s collaborators, including how they are deployed into a JNDI directory. This means that in spite of your good work, it is still possible for the session bean to fail in production: if you incorrectly deploy the objects on which the session bean depends, then the session bean will appear to fail. To avoid this rather unpleasant problem—one which you usually discover after you think you have finished your task—be sure to include those dependent objects in a test that verifies that they are correctly deployed into the live JNDI directory. See recipe 11.13, “Test the con- tent of your JNDI directory” for details.
At the time we write this, MockEJB supports session beans and message-driven beans, but not yet entity beans. We do not fully understand the limits of testing with MockEJB, but if you are testing legacy EJBs then you might be the one to find those limits. If you find you cannot get past them, then your next choice is to test the EJBs using Cactus, as we describe in 11.3 Test a session bean method in a real container. We favor MockEJB over Cactus, but recognize that MockEJB does not solve all our problems, so use MockEJB when you can and Cactus when you must.10
◆ Related
■ 11.1—Test a session bean method outside the container
■ 11.13—Test the content of your JNDI directory
■ MockEJB (www.mockejb.org)
■ Self-Shunt Pattern (www.objectmentor.com/resources/articles/SelfShunPtrn.pdf)
10Do not mistake this to mean that we dislike Cactus. Rather, we dislike testing domain logic inside a con- tainer, since domain logic should be separate from implementation details. We are glad that Cactus is there when we need it!
394 CHAPTER 11
Testing Enterprise JavaBeans