7.9 Testing Session Tracking Using HttpSession

7.9.1 Problem

You want to test that your servlet properly handles session tracking when using an HttpSession.

7.9.2 Solution

Write a ServletTestCase to test that your servlet properly handles adding and removing objects from an HttpSession.

7.9.3 Discussion

Servlet developers know that session tracking is critical for any web application that needs to maintain state between user requests. Since HTTP is a stateless protocol, it provides no way for a server to recognize consecutive requests from the same client. This causes problems with web applications that need to maintain information on behalf of the client. The solution to this problem is for the client to identify itself with each request. Luckily, there are many solutions to solving this problem. Probably the most flexible solution is the servlet session-tracking API. The session tracking API provides the constructs necessary to manage client information on the server. Every unique client of a web application is assigned a javax.servlet.http.HttpSession object on the server. The session object provides a little space on the server to hold information between requests. For each request, the server identifies the client and locates the appropriate HttpSession object.[9] The servlet may now add and remove items from a session depending on the user's request.

[9] An HttpSession, when first created, is assigned a unique ID by the server. Cookies and URL rewriting are two possible methods for the client and server to communicate this ID.

This recipe focuses on the popular "shopping cart." The shopping cart example is good because it is easy to understand. Our shopping cart is very simple: users may add and remove items. With this knowledge, we can write the first iteration of the servlet as shown in Example 7-7.

Example 7-7. First iteration of the ShoppingCartServlet
package com.oreilly.javaxp.cactus.servlet;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletException;
import java.io.IOException;

public class ShoppingCartServlet extends HttpServlet {

    public static final String INSERT_ITEM = "insert";
    public static final String REMOVE_ITEM = "remove";
    public static final String REMOVE_ALL = "removeAll";
    public static final String INVALID = "invalid";
    public static final String CART = "cart";

    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        HttpSession session = req.getSession(true);
        ShoppingCart cart = (ShoppingCart) session.getAttribute(CART);
        if (cart == null) {
            cart = new ShoppingCart(  );
            session.setAttribute(CART, cart);
        }
        updateShoppingCart(req, cart);
    }

    protected void updateShoppingCart(HttpServletRequest req,
                                      ShoppingCart cart)
            throws ServletException {
        String operation = getOperation(req);
        if (INSERT_ITEM.equals(operation)) {
            // @todo - implement adding item to the cart
        } else if (REMOVE_ITEM.equals(operation)) {
            // @todo - implement removing item from the cart
        } else if (REMOVE_ALL.equals(operation)) {
            // @todo - implement removing all items from the cart
        } else {
            throw new ServletException("Invalid Shopping Cart operation: " +
                                       operation);
        }
    }

    protected String getOperation(HttpServletRequest req) {
        String operation = req.getParameter("operation");
        if (operation == null || "".equals(operation)) {
            return INVALID;
        } else {
            if (!INSERT_ITEM.equals(operation)
                    && !REMOVE_ITEM.equals(operation)
                    && !REMOVE_ALL.equals(operation)) {
                return INVALID;
            }

            return operation;
        }
    }

    protected String getItemID(HttpServletRequest req) {
        String itemID = req.getParameter("itemID");
        if (itemID == null || "".equals(itemID)) {
            return INVALID;
        } else {
            return itemID;
        }
    }
}

When doGet( ) is called we ask the HttpServletRequest to give us the client's session. The true flag indicates that a new session should be created if one does not exist. Once we have the session, we look to see if a shopping cart exists. If a valid shopping cart does not exist, one is created and added to the session under the name ShoppingCartServlet.CART. Next, the updateShoppingCart( ) method is executed to either add or remove items from the shopping cart. The details for adding and removing items from the shopping cart are left unimplemented, allowing the tests to fail first. After a test fails, code is added to make the test pass.

Before we continue with the test, let's take a look at the support classes. A regular Java object called ShoppingCart represents our shopping cart. A ShoppingCart holds zero or more Java objects called Item. These objects are not dependent on server code and therefore should be tested outside of a server using JUnit. Example 7-8 and Example 7-9 show these objects.

Example 7-8. Shopping cart class
package com.oreilly.javaxp.cactus.servlet;

import java.io.Serializable;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;

public class ShoppingCart implements Serializable {

    private Map cart = new HashMap(  );

    public void addItem(Item item) {
        this.cart.put(item.getID(  ), item);
    }

    public void removeItem(String itemID) {
        this.cart.remove(itemID);
    }

    public Item getItem(String id) {
        return (Item) this.cart.get(id);
    }

    public Iterator getAllItems(  ) {
        return this.cart.values().iterator(  );
    }

    public void clear(  ) {
        this.cart.clear(  );
    }
}
Example 7-9. Shopping cart item class
package com.oreilly.javaxp.cactus.servlet;

import java.io.Serializable;

public class Item implements Serializable {

    private String id;
    private String description;

    public Item(String id, String description) {
        this.id = id;
        this.description = description;
    }

    public String getID(  ) {
        return this.id;
    }

    public String getDescription(  ) {
        return this.description;
    }
}

Objects used by an HttpSession should implement the java.io.Serializable interface to allow the session to be distributed in a clustered environment. The Item class is very basic, holding only an ID and description.

Now let's turn our attention to writing the Cactus tests. Example 7-10 shows how to test the addition of a new item to the shopping cart.

Example 7-10. Testing the addition of an item to a shopping cart
package com.oreilly.javaxp.cactus.servlet;

import org.apache.cactus.ServletTestCase;
import org.apache.cactus.WebRequest;

public class TestShoppingCartServlet extends ServletTestCase {

    private ShoppingCartServlet servlet;

    public TestShoppingCartServlet(String name) {
        super(name);        
    }

    public void setUp(  ) {
        this.servlet = new ShoppingCartServlet(  );
    }

    /**
     * Executes on the client.
     */
    public void beginAddItemToCart(WebRequest webRequest) {
        webRequest.addParameter("operation",
                                ShoppingCartServlet.INSERT_ITEM);
        webRequest.addParameter("itemID", "12345");
    }

    /**
     * Executes on the server.
     */
    public void testAddItemToCart(  ) throws Exception {
        this.servlet.doGet(this.request, this.response);
        Object obj = this.session.getAttribute(ShoppingCartServlet.CART);
        assertNotNull("Shopping Cart should exist.", obj);
        assertTrue("Object should be a ShoppingCart",
                   obj instanceof ShoppingCart);
        ShoppingCart cart = (ShoppingCart) obj;
        Item item = cart.getItem("12345");
        assertNotNull("Item should exist.", item);
    }
}

The test starts execution on the client. In this example, the method under test is testAddItemToCart. Cactus uses reflection to locate a method called beginAddItemToCart(WebRequest) to execute on the client. The beginAddItemToCart(WebRequest) method adds two parameters to the outgoing request. The parameter named operation is assigned a value telling the shopping cart servlet to add an item to the shopping cart. The itemID parameter specifies which item to look up and store in the shopping cart. Next, Cactus opens an HTTP connection with server and executes the test method testAddItemToCart( ) (remember testXXX( ) methods are executed on the server). The testAddItemToCart( ) explicitly invokes the doGet( ) method, which performs the necessary logic to add a new item to the shopping cart. The test fails because we have not yet implemented the logic to add a new item to the shopping cart. Example 7-11 shows the updated servlet adding an item to the shopping cart.

Example 7-11. Updated ShoppingCartServlet (add item to the shopping cart)
protected void updateShoppingCart(HttpServletRequest req,
                                  ShoppingCart cart) 
        throws ServletException {
    String operation = getOperation(req);
    if (INSERT_ITEM.equals(operation)) {
        addItemToCart(getItemID(req), cart);
    } else if (REMOVE_ITEM.equals(operation)) {
        // @todo - implement removing item from the cart
    } else if (REMOVE_ALL.equals(operation)) {
        // @todo - implement removing all items from the cart.
    } else {
        throw new ServletException("Invalid Shopping Cart operation: " +
                                   operation);
    }
}

protected void addItemToCart(String itemID, ShoppingCart cart) {
    Item item = findItem(itemID);
    cart.addItem(item);
}

protected Item findItem(String itemID) {
    // a real implementation might retrieve the item from an EJB.
    return new Item(itemID, "Description " + itemID);
}

Executing the tests again results in the test passing. Writing the tests for removing items from the cart follows the same pattern: write the test first, watch it fail, add the logic to the servlet, redeploy the updated code, run the test again, and watch it pass.

7.9.4 See Also

Recipe 7.8 shows how to test cookies. Recipe 7.10 shows how to test initialization parameters.