Test updating session data without a container

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

Problem

You have logic that updates an HTTP session and you would like to test it.

Background

You have chosen to store temporary, client data in the HTTP session, as opposed to a stateful session bean or some other mechanism. You would like to verify that the session is updated correctly, without involving the entire web application in the process. End-to-End Tests are typically quite long, as they can require many steps just to get the application to the desired point. Once you reach that point, you have to rely on the application to correctly interpret the session data just to verify that the session data is correct. What if there is a defect when displaying ses- sion data? How do you know which part works?

The real problem is that many applications scatter their interaction with the ses- sion all over the place, either duplicated within the servlet or in a variety of places outside the servlet. Duplication, as always, is the enemy. The question is how to refactor to make this logic available outside the container, yet allow it to interact with the session.

Recipe

There are two key responsibilities at play in this interaction: updating session data and then updating the HTTP session object. The distinction between the session and its data is the key point for this recipe. Your business logic does not need to know where the data comes from: some from the request, some from the session, some from a hole in the wall—to the business logic it is not important. Therefore, we recommend you do the following:

1 Move the logic that updates the session data to its own class, usually called an action.

2 Keep the logic that updates the HTTP session with the session data in the servlet.

3 At each request, have the servlet take a snapshot of the session data and pass that to the action for processing.

To illustrate this, we will follow a time-honored tradition: implementing a shop- ping cart with an HTTP session. Of course, in a real e-commerce application you would never store the shopping cart in a user’s session; but much as “Hello, World”

447 Test updating session data

without a container

is the obligatory example of a first program, so is the shopping cart the obligatory HTTP session example. Our store is a coffee shop, selling coffee beans of different varieties and, one hopes, in copious quantities. You can surf our online store and purchase coffee beans by the kilogram.2 When you submit the form to add a few kilograms of Sumatra to your shopcart, this code takes over:

HttpSession session = request.getSession(true);

for (Iterator i = requestedQuantities.iterator(); i.hasNext();) { CoffeeQuantity each = (CoffeeQuantity) i.next();

Integer currentQuantityInKilograms =

(Integer) session.getAttribute(each.getCoffeeName());

if (currentQuantityInKilograms == null) { session.setAttribute(

each.getCoffeeName(),

new Integer(each.getAmountInKilograms()));

} else {

int newQuantityInKilograms =

currentQuantityInKilograms.intValue() + each.getAmountInKilograms();

session.setAttribute(

each.getCoffeeName(),

new Integer(newQuantityInKilograms));

} }

This codes lives inside the servlet3 and is invoked by the method doPost(). Here, requestedQuantities is a collection of CoffeeQuantity objects, each of which describes the amount of a certain type of coffee. For example, if you ask for 3 kg of Special Blend, the corresponding CoffeeQuantity object has the values in table 12.1.

2 With a Canadian author, you get kilograms. If you want pounds, multiply by 2.2.

3 We are talking about a hypothetical servlet that stores session information this way. The actual servlet in our Coffee Shop application, CoffeeShopController, has already been refactored according to this recipe.

Table 12.1 Sample CoffeeQuantity properties Property name Property value

amountInKilograms 3

coffeeName “Special Blend” (java.lang.String)

448 CHAPTER 12

Testing web components

Then when it is time to display your shopcart, this code takes over:

public static ShopcartBean create(

HttpSession session, CoffeeCatalog catalog) {

ShopcartBean shopcartBean = new ShopcartBean();

for (Enumeration e = session.getAttributeNames();

e.hasMoreElements();

) {

String eachCoffeeName = (String) e.nextElement();

Integer eachQuantityInKilograms =

(Integer) session.getAttribute(eachCoffeeName);

ShopcartItemBean item = new ShopcartItemBean(

eachCoffeeName,

eachQuantityInKilograms.intValue(), catalog.getUnitPrice(eachCoffeeName));

shopcartBean.shopcartItems.add(item);

}

return shopcartBean;

}

This code lives within the ShopcartBean, the object that contains all the shopcart data to be displayed on a web page. Notice that it too interacts directly with the HTTP ses- sion, in spite of the fact that this object has the potential to be used outside the con- text of a web application. This is an indicator of high coupling in the design. If you are not in a position to extract the business logic from this code, then you can use ServletUnit to test it, which we describe in recipe 12.2, “Test updating the HTTP ses- sion object.”

We want to test the logic that updates the shopcart, pure and simple. We want to write this test:

public void testAddToEmptyShopcart() { String coffeeProductId = "0";

String coffeeName = "Sumatra";

int requestedQuantity = 5;

CoffeeCatalog catalog = new CoffeeCatalog();

catalog.addCoffee(

coffeeProductId, coffeeName, Money.dollars(7, 50));

ShopcartModel model = new ShopcartModel();

List requestedQuantities = Collections.singletonList(

new CoffeeQuantity(

requestedQuantity,

449 Test updating session data

without a container

catalog.lookupCoffeeById(coffeeProductId)));

model.addCoffeeQuantities(requestedQuantities);

assertEquals(5, model.getQuantity("Sumatra"));

assertEquals(5, model.getTotalQuantity());

}

This test primes the catalog with data, creates a new shopcart, adds a certain quan- tity of coffee to the shopcart, then verifies both the amount of Sumatra coffee and the amounts of all coffees. The last assertion ensures that the Sumatra is the only coffee in the shopcart. This is much more to the point. To write this test, we need to make the design change indicated in figure 12.1.

Notice that the preceding test says nothing whatsoever about HTTP session, requests, or servlets. It is a pure test of business logic. You can see the final Shop- cartModel code in listing 12.1:

package junit.cookbook.coffee.model;

import java.io.Serializable;

import java.util.*;

import com.diasparsoftware.java.util.Quantity;

public class ShopcartModel implements Serializable { private Map coffeeQuantities = new HashMap();

public void addCoffeeQuantities(List requestedQuantities) { for (Iterator i = requestedQuantities.iterator();

i.hasNext();

) {

CoffeeShop Controller (HttpServlet)

ShopcartModel getQuantity(coffeeName) addQuantity(coffeeQuantity) CoffeeShop

Controller (HttpServlet)

HttpSession

Map of coffee names to

shopcart quantities

Figure 12.1

Changing the design to encapsulate session data in a model object

Listing 12.1 ShopcartModel

450 CHAPTER 12

Testing web components

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

String coffeeName = each.getCoffeeName();

CoffeeQuantity currentQuantity = getCoffeeQuantity(coffeeName);

Quantity sum = each.add(currentQuantity);

coffeeQuantities.put(coffeeName, sum);

} }

private CoffeeQuantity getCoffeeQuantity(String coffeeName) { CoffeeQuantity currentQuantity =

(CoffeeQuantity) coffeeQuantities.get(coffeeName);

return (currentQuantity == null) ? new CoffeeQuantity(0, coffeeName) : currentQuantity;

}

public int getQuantity(String coffeeName) { return getCoffeeQuantity(coffeeName) .getAmountInKilograms();

}

public int getTotalQuantity() { int totalQuantity = 0;

for (Iterator i = coffeeQuantities.values().iterator();

i.hasNext();

) {

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

totalQuantity += each.getAmountInKilograms();

}

return totalQuantity;

}

public Iterator items() {

return coffeeQuantities.values().iterator();

}

public boolean isEmpty() {

return coffeeQuantities.isEmpty();

}

public boolean equals(Object other) {

if (other != null && other instanceof ShopcartModel) { ShopcartModel that = (ShopcartModel) other;

return this.coffeeQuantities .equals(that.coffeeQuantities);

} else {

return false;

} }

TE AM FL Y

Team-Fly®

451 Test updating session data

without a container

public int hashCode() {

return coffeeQuantities.hashCode();

}

public String toString() {

return "a ShopcartModel with " + coffeeQuantities;

} }

We have changed the servlet so that interaction with the HTTP session is reduced to a single method.

public ShopcartModel getShopcartModel(HttpServletRequest request) { HttpSession session = request.getSession(true);

ShopcartModel model =

(ShopcartModel) session.getAttribute("shopcartModel");

if (model == null) {

model = new ShopcartModel();

session.setAttribute("shopcartModel", model);

}

return model;

}

This is the entire interface between your business logic and HTTP session, and although it is not too simple to break, it is so simple that defects in your business logic will not affect your interaction with the session. To test your interaction with the session, you only need the following tests:

■ Start with an empty session. Issue a request. Expect a shopcart model in the session.

■ Start with a session containing a shopcart model. Issue a request. Expect the shopcart model to be there.

With these two tests in place, you can ignore HTTP session interaction when test- ing the rest of your application.

Discussion

The original design was simple from one perspective, but there were two proper- ties of the design preventing us from writing the test we wanted to write.

■ The logic to update the shopcart was tightly coupled to the Controller—the servlet.

■ The logic to update the shopcart was in a different place than the logic to retrieve the shopcart.

452 CHAPTER 12

Testing web components

The first design property made it difficult to execute the update logic on its own, forcing us to drag the servlet along for the ride. We should be able to test the update logic no matter how the application delivers the data to it, and our final test reflects that statement, because the test provides the data.

The second design property made it difficult to have confidence in the servlet’s ability to update the session correctly with the session data. Keeping the HTTP ses- sion up to date after changing the underlying session data should almost be auto- matic. At a minimum, it should only occur in one place. If you find that your session data object works correctly, but you still have session problems, then the problem lies in the code that takes your single session model object (like our ShopcartModel) and stuffs it into the HTTP session. In this case, we recommend writing a few tests to ensure that your session data object makes it into the HttpSession properly (see recipe 12.2 for details on how to do this). You can then refactor to a design where this “glue code” appears in only one place. After that, you can concentrate on having the right session data object without worrying about whether it actually gets into the session.

Related

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

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

(753 trang)