Test a session bean method outside the container

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 409 - 418)

Problem

You want to test a session bean method without involving the container.

Background

If you have previous experience trying to test EJBs in a live container, you under- stand the issues involved. It takes time to start and stop the container: we have worked on projects where starting the EJB server took fifteen minutes! So even in the days before we practiced Test-Driven Development, or indeed automated test- ing at all, we were sometimes only able to test our EJB logic about three times per hour. This constrained our ability to program incrementally and forced us to write more code in between testing sessions, which made it difficult to know where a newly injected defect was, slowing us down even more. This is a positive feedback loop of negative feelings, and if nothing else, is bad for morale.

Next, there is the complexity of deployment. Especially in the days before we understood EJBs (but were nevertheless expected to write them), deploying EJBs happened as if by magic. It is quite similar to seeing public static void main(String[]args){} for the first time. If you did not have a background in the C language when you learned Java, then this statement must have looked like a strange way to say, “This is the entry point of my application.” There is also a fair amount of strange machinery that one needs to use even to deploy the simplest

“Hello, World” session bean. This creates a barrier for the programmer trying to take their first small step in building a new session bean method. Even when you become experienced enough that deploying EJBs is a familiar task, it is slow, so you would like to defer deploying your EJB to the last possible moment. This rec- ipe can help.

Recipe

The general strategy of this recipe, as with many of the J2EE-related recipes in this book, is to refactor towards a more testable design. We recommend that you extract your business logic from the session bean method and place it in a Plain

379 Test a session bean method

outside the container

Old Java Object (POJO). The resulting session bean, with its business logic removed, then plays the role of Remote Facade [PEAA, 388], and generally becomes too simple to break (see the essay B.1, “Too simple to break”). At this point, you can use the techniques in part 1 to test the business logic POJO entirely in memory. You can achieve all this by applying a refactoring similar to Move Method [Refactoring, 142].

Rather than move the session bean method, you move the implementation of the method (but not the method itself) into a new Domain Model class, and then change the session bean method to delegate its work to the new Domain Model class. To illustrate this we return to our Coffee Shop application and the feature that displays the shopcart, which has been implemented by a ShopcartOperations session bean. We have a method that adds coffee to the shopcart and another method that retrieves the shopcart on demand. The Controller is now blissfully ignorant of where or how the shopcart is stored. Listing 11.1 shows the code for the session bean implementation, with some of the irrelevant code omitted. The create()method creates an empty shopcart and the methods addToShopcart() and getShopcartItems() behave as their names suggest.

package junit.cookbook.coffee.model.ejb;

import java.util.*;

import junit.cookbook.coffee.model.CoffeeQuantity;

public class ShopcartOperationsBean implements javax.ejb.SessionBean { private Map coffeeQuantities;

public void ejbCreate() throws javax.ejb.CreateException { coffeeQuantities = new HashMap();

}

public void addToShopcart(Vector requestedCoffeeQuantities) { for (Iterator i =

requestedCoffeeQuantities.iterator();

i.hasNext();

) {

CoffeeQuantity each = (CoffeeQuantity) i.next();

String eachCoffeeName = each.getCoffeeName();

CoffeeQuantity currentQuantity;

if (coffeeQuantities

.containsKey(eachCoffeeName)) { Listing 11.1 ShopcartOperationsBean

380 CHAPTER 11

Testing Enterprise JavaBeans

currentQuantity =

(CoffeeQuantity) coffeeQuantities.get(

eachCoffeeName);

} else {

currentQuantity =

new CoffeeQuantity(0, eachCoffeeName);

coffeeQuantities.put(

eachCoffeeName, currentQuantity);

}

coffeeQuantities.put(

eachCoffeeName,

currentQuantity.add(each));

} }

public Vector getShopcartItems() {

return new Vector(coffeeQuantities.values());

} }

The test we have in mind simulates two transactions, adding coffees A and B to the shopcart first, and then adding coffees A and C second. This is perhaps the most complex test we can imagine for adding coffee to the shopcart, because the second transaction adds a new product and adds more of a product already in the shopcart. If this test passes, then we feel confident that the others will pass. This is the test we want to write:

public void testComplexCase() throws Exception { // Create a shopcart somehow!

Vector coffeeQuantities1 = new Vector();

coffeeQuantities1.add(new CoffeeQuantity(2, "A"));

coffeeQuantities1.add(new CoffeeQuantity(3, "B"));

shopcart.addToShopcart(coffeeQuantities1);

assertEquals(coffeeQuantities1, shopcart.getShopcartItems());

Vector coffeeQuantities2 = new Vector();

coffeeQuantities2.add(new CoffeeQuantity(1, "A"));

coffeeQuantities2.add(new CoffeeQuantity(2, "C"));

shopcart.addToShopcart(coffeeQuantities2);

Vector expectedTotalQuantities = new Vector();

expectedTotalQuantities.add(new CoffeeQuantity(3, "A"));

expectedTotalQuantities.add(new CoffeeQuantity(3, "B"));

TE AM FL Y

Team-Fly®

381 Test a session bean method

outside the container

expectedTotalQuantities.add(new CoffeeQuantity(2, "C"));

assertEquals(expectedTotalQuantities, shopcart.getShopcartItems());

}

Notice that there is nothing in this test that mentions EJBs. Also notice that we have not yet been able to write the line of code that creates the shopcart object we want to test. This is the point at which we create a new Domain Model object rep- resenting the shopcart business logic and add to it the appropriate methods. We call this new class ShopcartLogic, which allows us to replace the opening com- ment in the code with this line of code:

ShopcartLogic shopcart = new ShopcartLogic();

We now copy the two methods, addToShopcart() and getShopcartItems() from the session bean implementation class into ShopcartLogic. When we do this, we realize that we also need to copy the session bean’s client state (the variable coffeeQuantities) into ShopcartLogic, or the latter does not compile. The result is the class shown in listing 11.2.

package junit.cookbook.coffee.model;

import java.util.*;

public class ShopcartLogic { private Map coffeeQuantities;

public void addToShopcart(Vector requestedCoffeeQuantities) { for (Iterator i =

requestedCoffeeQuantities.iterator();

i.hasNext();

) {

CoffeeQuantity each = (CoffeeQuantity) i.next();

String eachCoffeeName = each.getCoffeeName();

CoffeeQuantity currentQuantity;

if (coffeeQuantities

.containsKey(eachCoffeeName)) { currentQuantity =

(CoffeeQuantity) coffeeQuantities.get(

eachCoffeeName);

} else {

currentQuantity =

new CoffeeQuantity(0, eachCoffeeName);

Listing 11.2 ShopcartLogic

382 CHAPTER 11

Testing Enterprise JavaBeans

coffeeQuantities.put(

eachCoffeeName, currentQuantity);

}

coffeeQuantities.put(

eachCoffeeName,

currentQuantity.add(each));

} }

public Vector getShopcartItems() {

return new Vector(coffeeQuantities.values());

} }

When we execute our test, it fails with a NullPointerException. We do not initial- ize the coffeeQuantities collection in ShopcartLogic. To fix this problem, we copy the body of the session bean’s ejbCreate() method into a new constructor for ShopcartLogic. Here is the resulting constructor:

public ShopcartLogic() {

coffeeQuantities = new HashMap();

}

We execute the test one more time and receive the following failure:

junit.framework.AssertionFailedError: expected:

<[<3, A>, <3, B>, <2, C>]> but was:<[<3, A>, <2, C>, <3, B>]>

Looking at the failure, we see that the expected shopcart contents and the actual shopcart contents are the same, but that the items appear in a different order.

This test should pass, but when we compare Vector objects for equality, the index at which each element is stored affects whether the collections are equal (see rec- ipe 2.9, “Let collections compare themselves”). We can immediately think of two ways to fix this problem:

■ Compare the expected and actual shopcart contents in a way that ignores the index of each element.

■ Return the shopcart items as a Set, rather than as a Vector.

When we implemented the session bean we chose Vector because Vector is serial- izable, and the EJB specification requires all method parameters and return types for remote EJB methods to be serializable at runtime.5 To keep confusion to a minimum, we specified Vector rather than rely on ourselves to get the runtime

5 This is a slight simplification. These types may be String, primitives, implementations of javax.rmi.

Remote or of java.io.Serializable. See the EJB specification for details.

383 Test a session bean method

outside the container

type right.6 The Set interface does not extend Serializable, so there is no guar- antee that all Set implementations are serializable, in spite of the fact that HashSet and TreeSet—the two basic implementations of Set in Java—are each serializable.

While deciding whether to change the return type of getShopcartItems() from Vector to Set (which has a slight ripple effect in our design), we can get to a

“quick green” by wrapping both the expected and actual shopcart items in Set objects and comparing those. This is one of those tricks that Java programmers typically learn about through experience, rather than through a tutorial. We change the final assertion of our test to the following:

assertEquals(

new HashSet(expectedTotalQuantities), new HashSet(shopcart.getShopcartItems()));

Finally the test passes. We can now change the session bean method to delegate to this Domain Model object. Listing 11.3 shows the final session bean implementation code, or at least the relevant parts. We have highlighted the changes in bold print.

package junit.cookbook.coffee.model.ejb;

import java.util.Vector;

import junit.cookbook.coffee.model.ShopcartLogic;

public class ShopcartOperationsBean implements javax.ejb.SessionBean {

private javax.ejb.SessionContext mySessionCtx;

private ShopcartLogic shopcart;

public void ejbCreate()

throws javax.ejb.CreateException { shopcart = new ShopcartLogic();

}

public void addToShopcart(Vector requestedCoffeeQuantities) { shopcart.addToShopcart(requestedCoffeeQuantities);

}

public Vector getShopcartItems() { return shopcart.getShopcartItems();

} }

6 If we were doing it all again, we would change the public interface to List or Collection, rather than Vector. The good news is that we can always refactor.

Listing 11.3 The final version of ShopcartOperationsBean

384 CHAPTER 11

Testing Enterprise JavaBeans

This session bean implementation class is too simple to break: it does nothing more than delegate method invocations to another object. Such a class cannot break unless there is a defect in the class to which it delegates, which is ShopcartLogic in our case. If it cannot break, why test it? Test the underlying business logic, instead, as we have done.

Let us summarize what we did, so that you can apply this technique to your own session beans:

1 We created a new Domain Model class that implements the same meth- ods that are on the session bean’s remote component interface.

2 We wrote a test that uses the new Domain Model class rather than the session bean implementation class.

3 We copied the body of the methods we wanted to test from the session bean implementation class into the Domain Model class.

4 We copied the relevant instance variables (fields) into the Domain Model class.

5 We copied the body of any relevant ejbCreate() method into a corre- sponding constructor in the Domain Model class.

6 We tested the Domain Model methods until we were satisfied that they work.

7 We removed the body of each session bean implementation method, replacing it with a simple invocation of the Domain Model’s correspond- ing method. Those session bean methods are now too simple to break.

From here we simply repeat this process until we have moved all business logic from the session bean implementation classes into the Domain Model classes. You can apply this same technique to all your session bean methods, whether stateless or stateful. After you do, you can test all your business logic entirely outside the container, with tests that execute at a rate of hundreds per second rather than a few per second, as we have experienced in past projects.

Discussion

If you use this technique to refactor an existing session bean layer, you might be put off by the extra classes you begin to write. You will feel as though you are dou- bling a large part of your code base “just for testing.” First, we feel that testing is important enough to warrant the extra code: we would rather have twice as much tested code than a smaller amount of untested code. Still, we recognize that extra

385 Test a session bean method

outside the container

code of any kind introduces its own costs: the programmers need to read it and understand it. All things being equal, less code is better. Can we avoid this extra code somehow? Yes. In many cases it is possible to instantiate your session bean implementation class directly and test its methods just as you test any other method that returns a value. In particular, any session bean method that does not invoke services the EJB container provides (such as a JNDI lookup) can be tested this way. In our Coffee Shop example, we might write the test as follows:

package junit.cookbook.coffee.model.logic.test;

// import statements omitted

public class AddToShopcartSessionBeanTest extends TestCase { public void testComplexCase() throws Exception {

ShopcartOperationsBean shopcart = new ShopcartOperationsBean();

shopcart.ejbCreate();

// Rest of the test as written previously }

}

If you choose this technique, do not forget to invoke ejbCreate()! This method acts as the implementation class’s “second constructor,” so calling only its (Java) con- structor is insufficient. One drawback to this approach is that the test now depends on part of the J2EE interface: namely CreateException, which ejb- Create() might throw. This makes it necessary to add a part of the J2EE library (in our case it was j2ee.jar) to your class path when building and executing your tests.

Is this a small price to pay for a simpler design? We leave it up to you to try them both and judge for yourself. If someone decides tomorrow that your business logic layer ought to use Jini for object distribution rather than EJB, then you will need to perform the refactoring this recipe suggests, anyway.

NOTE A testable design is a better design—George Latkiewicz, one of our most pro- lific reviewers, emphasized to us the point underlying the previous para- graph: this is yet another case where designing for testability naturally improves the design in other ways. He wrote, “By following the refactor- ing recommended in this recipe, the EJBs become what they should have been all along—merely wrappers that provide the benefits and services of the container to the guts of your application. Now an EJB guru can make sure that the wrappers do the ‘right thing the right way’ without being distracted by lots of application logic code, and similarly, the application developer can inspect the domain classes without needing to understand all the intricacies of EJB.

“If it weren’t for all the hype that led to the EJB bandwagon, one wouldn’t have even considered a framework which required such an inva-

386 CHAPTER 11

Testing Enterprise JavaBeans

sive solution to the problems that EJB addresses (distribution, transac- tions). So, what this recipe reminds us of is that EJB doesn’t really force us to do what we shouldn’t be doing. It is interesting to note that many of the new emerging frameworks (Spring, JDO, JBoss 4) have, as a central design goal, the theme: “leave my POJO alone.”

We could not agree more.

Some of your session bean methods retrieve objects through JNDI. Because this is a service that your application server provides, you cannot move this code into a Plain Old Java Object and expect it to work outside the application server runtime environment. We suggest you treat this object as a parameter to your business logic, and it is up to your application (and the distributed objects services it uses) to decide how to retrieve it. Add that object to the Domain Model business method interface as a parameter, and then have the session bean retrieve the object using JNDI and pass it into the Domain Model business method. Your test can provide that object any way it likes, including passing in hard-coded values, an in-memory implementation, or a mock object (see recipe 11.6). You will use this technique when your session bean uses an entity bean to provide business data persistence. The session bean retrieves the entity bean by JNDI and passes it as a parameter to the Domain Model. In your test, provide some dummy implementa- tion of the entity bean’s component interface (remote or local, depending on your needs) so that you can test the business logic without worrying about the cor- rectness of your persistence mechanism. Test your entity beans on their own, using the other recipes in this chapter.

It is important to understand why we use EJB technology. We introduce EJBs into our J2EE application designs to provide object distribution, declarative secu- rity, and declarative transactions—and nothing more. When we incorporate EJBs into an application, the goal is to add these features to existing business logic, so we should be able to test the business logic without worrying about EJBs. Even if it is “too late” for the session beans that someone has already written, if you decide to write all new session beans to be mere Remote Facades, then you will find your- self building those session beans more quickly than you did before.

That is all well and good when considering future session beans, but what if you need to test existing session beans that you are not allowed to refactor? In that case, you need to either simulate the container (see recipe 11.2) or test in a live environment (see recipe 11.3). The resulting tests are slower to execute and often more complex to write, but you have no choice. Good luck.

387 Test a legacy session bean

Related

■ 2.9—Let collections compare themselves

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 409 - 418)

Tải bản đầy đủ (PDF)

(753 trang)