JaVa
   

Case Study: The Pet Store with Custom Tags

So far, we have explored the access Cactus provides to the servlet container and the ways in which Cactus test cases can be leveraged to unit- or integration-test various J2EE components. Now it's time to put the rubber to the road and see how Cactus-based unit tests can verify our code and speed the refactoring process on a sample app. We will use Cactus to test a custom tag we develop to simplify the JSPs used in the front end of the AAA Pets online store.

The Business Need

Let's suppose that the developers who designed the JSPs for the front end of the AAA pet store were more concerned with getting a site up and running than with design. Now, several months after the initial launch, the AAA marketing director has authorized a redesign. Furthermore, in the time since development began, our shop has adopted extreme programming. As a result, our XP coach reminds us constantly of the value of adding unit tests to the existing code. The original JSPs contain a lot of logic, and a few near-disasters have reminded us of the fragility of untested code. This week, we are scheduled to refactor the JSPs to use custom tags, separating some of the logic so that we can test it.

Finding a Starting Point

We begin looking over the JSPs. The J2EE blueprints available on Sun's Web site suggest that "JSP pages should contain no code written in the Java coding language (that is, no expressions or scriptlets). Anything a JSP page needs to do with Java code can be done from a custom tag" (http://java.oracle.com/ j2ee/blueprints/web_tier/qanda/index.html). We ponder that for a while and begin to wonder at the enormity of our task. We decide to keep the blueprint's suggestion as our eventual goal and tackle a small chunk first. After an examination of the main navigation pages, we find a for loop whose basic structure appears on two separate pages. The loop iterates through all the subcategories available in the current category and prints out a link to each one. The following listing displays the code fragment from header.jsp.

<%
 Category category = categorySystem.getCurrentCategory();
 Subcategory [] subcategories = category.getSubcategories();
 for (int index=0; index < subcategories.length; index++){
 Subcategory subcategory = subcategories[index];
 %>
 <td width="20%" align="center">
 <a href="subcategory.jsp?id=<%=subcategory.getid()%>">
 <%=subcategory.getName()%>
 </a>
 <br />
 </td>
<%}%>


subcategory.jsp creates a similar loop (the contents do not include table cells) that displays the subcategories in the sidebar. The IterationTag interface provides easy hooks into just this sort of functionality. This seems to be a likely place to begin our refactoring, and we sit down to work.

The TestCase

We begin by writing the test first, a practice recommended by XP that helps us think through the problem domain before attempting to solve it. "What will our custom tag have to accomplish? " we ask ourselves. We decide that the custom tag should assume responsibility for obtaining a valid list of subcategories. Then, the tag should step through the subcategories and print the body of the tag once for each member of the array. However, we need to vary some elements of the body on each pass. The old solution does this by making method calls in scriptlets:

<%=subcategory.getName()%> 


We decide to move this Java code into the custom tag and create a couple of temporary variables that exist strictly within the body of the tag and change their values with each body iteration.

The setUp() Method

To test custom tags, we extend JspTestCase. Then, we proceed with the setup code that is common to almost all custom tags: creating the tag, setting its page context, and calling doStartTag(): (We don't know that we'll need doStartTag for sure, but doStartTag often serves to initialize the "first pass" of the tag—we guess we'll need it in a minute.)

private SubcategoryListTag tag;
private CategorySystem categorySystem;
public void setUp()throws Exception{
 categorySystem = new CategorySystem();
 session.setAttribute("categorySystem", categorySystem);
 tag = new SubcategoryListTag();
 tag.setPageContext(this.pageContext);
 tag.doStartTag();
}


In addition to the tag setup, we map an instance of the CategorySystem model object into the session. We allow this dependency because most of the existing pages do this already using a <jsp:useBean> tag.

The Tag Loop

Now that our tag is ready to run, we need to write code that verifies its entire life cycle. Because we know it will iterate, we type in the standard tag do-while loop used by most servlet containers:

do{
}while(tag.doAfterBody() == tag.EVAL_BODY_AGAIN);


Now we ask "What do we need to test? " We must check if we have created our scripting variables, and according to the JSP specification, these variables are made available to the container by nesting them in one of the accessible scopes (usually the PAGE scope). This approach seems promising, so we code to pull the variables from where they should have been placed by the tag:

do{
 pageContext.getAttribute("subCatId");
 pageContext.getAttribute("subCatName");
}while(tag.doAfterBody() == tag.EVAL_BODY_AGAIN);


"But how," we wonder, "will we verify that these objects are correct? " We could set up an array that parallels the one used by the loop, and we could even get the data the same way. This seems like a good verification, so we implement this logic and add a final check to verify that the tag iterates once per element in the array (lest the test terminate prematurely). Now that we've completed the test, it's time to write the actual code. The next listing contains the final version of the test method.

public void testList() throws Exception{
 Category category = categorySystem.getCurrentCategory();
 Subcategory [] subcategories = category.getSubcategories();
 int count = 0;
 do{
 Subcategory subCat = subcategories[count++];
 int id = subCat.getId();
 assertEquals(""+id, pageContext.getAttribute("subCatId"));
 String name = subCat.getName();
 assertEquals(name, pageContext.getAttribute("subCatName"));
 }
 while (tag.doAfterBody() == tag.EVAL_BODY_AGAIN);
 /*tag has repeated as many times as there are array members*/
 assertEquals(count,subcategories.length); }


Writing the Tag

Writing the test simplifies writing the code. We have the following facts clearly in mind:

We begin to brainstorm about code that fulfills these criteria. An iterator would keep track of our position, and it has a handy hasNext() method that we can use to determine the return value for doAfterBody(). Using this utility class prevents us from having to maintain an internal count and do numeric comparisons. My first impulse had been to use the array without the iterator, since that's what the original JSP did. Because we have a test to validate the class, we don't feel so bound to the previous implementation. Already the tests have helped. We set to work. We write a private helper method to initialize our iterator (stored in an instance member variable). Aside from some exception checking, the method is straightforward: First, we pull a CategorySystem object from the session scope using pageContext. Then, we retrieve an array of subcategories to iterate over. Finally, we use the static method asList() from java.util.Arrays to turn the array into a java.util.List, and from there we get its iterator. The code for the helper method is shown here:

private Iterator iter;
public int doStartTag() throws JspTagException{
 initializeIterator();
 if(!iter.hasNext()){
 return SKIP_BODY;
 }
 updatePageVariables();
 return EVAL_BODY_INCLUDE;
}

private void initializeIterator() throws JspTagException{
 CategorySystem categorySystem = (CategorySystem) pageContext.getAttribute("categorySystem",
 pageContext.SESSION_SCOPE);
 if(categorySystem == null){
 throw new JspTagException("categorySystem not found in session");
 }
 Category category = categorySystem.getCurrentCategory();
 Subcategory [] subcategories = null;
 try{
 subcategories = category.getSubcategories();
 }
 catch(Exception e){
 throw new JspTagException("subcategories cannot be retrieved: " + e);
 }
 //iter is an instance variable.
 iter = Arrays.asList(subcategories).iterator();
}


We use another private helper method to update the scripting variables in the pageContext, and we use Iterator.hasNext() to determine the return value of doAfterBody():

public int doAfterBody(){
 if(iter.hasNext()){
 updatePageVariables();
 return EVAL_BODY_AGAIN;
 }
 return SKIP_BODY;
}
private void updatePageVariables(){
 Subcategory subCat = (Subcategory)iter.next();
 /*new values repalce previous ones, if any*/
 pageContext.setAttribute("subCatId", String.valueOf(subCat.getId()));
 pageContext.setAttribute("subCatName", subCat.getName());
}


Now all we have to do is build and run the tests. We discover that they all pass! Next, we ask ourselves, "Have we tested everything that could possibly break? " We haven't put the tag into its native JSP context yet, and until we do we can't be sure of it. We whip up a one-tag JSP to demonstrate use and verify that our tag library descriptor also works. We use the method outlined in the section "Tag Library Descriptor Testing" earlier in this chapter—using pageContext.include() to load and process the JSP. The following listing gives the test method from the JspTestCase as well as the test JSP itself.

/*test method from Catus test case*/
public void testListWithJsp() throws Exception{
 pageContext.include("subcategory_list_test.jsp");
}
/**** subcategory_list_test.jsp ****/
<%@taglib uri="WEB-INF/petstore-taglib.tld" prefix="petstore"%>

<petstore:subcategoryList>
 Name = <%= subCatName%>.<br>
 ID = <%= subCatId %>
</petstore:subcategoryList>


The error message Cactus dumps when we run the improved test reveals that we never wrote the tag library descriptor entry! (Unit tests are great for catching coarse errors as well as subtle ones.) We quickly write the deployment descriptor entry:

<tag>
 <name>subcategoryList</name>
 <tagclass>xptoolkit.petstore.tag.SubcategoryListTag</tagclass>
 <info>
 Iterates over all of the available subcategories. Provides two nested variables, "subCatName" and "subCatId" corresponding to
 same attributes of current subcategory.
 </info>
 <bodycontent>JSP</bodycontent>
 <variable>
 <name-given>subCatId</name-given>
 <declare>true</declare>
 <!-- default is nested -->
 </variable>
 <variable>
 <name-given>subCatName</name-given>
 <declare>true</declare>
 <!-- default is nested -->
 </variable>
</tag> 


Once we verify that our code passes all our automated tests, we begin adding the tag to our JSPs. Here is the end result of one such replacement:

<petstore:subcategoryList>
 <a href="subcategory.jsp?id=<%= subcatid%>"><%= subCatName %></a>
 <br>
</petstore:subcategoryList>


One minor issue crops up while we are manually verifying that the pages do indeed still work (in a way, we are testing our tests). The variable names we have defined inside the subcategoryList tag conflict with other variables in the JSPs. We make the change (a very minor one), run our tests, and redeploy. Now all the modified pages (along with our tag code) have become production candidates. Because our tests let an error through, however, we are not content. We decide to add a link-checking spider to the suite of tests that are run with each build. I concur, and we add functional testing to our list of tasks.

Review of the Case Study

During this refactoring episode, we pulled code out of JSPs (the view in an MVC architecture) and encapsulated it in JSP custom tags. In the process, we made portions of the site's logic amenable to testing. This result represents a victory. Every piece of code that is paired with a unit test is a safe piece of code—one that you're not afraid to touch and redesign. Would we have written these tests without Cactus? Maybe not. Faced with mocking up portions of the servlet specification (and JSP, and custom tag specifications to boot!), even the most dedicated coding pair might have released their inner pig and relied on total visual inspection. Not only do we have better code for the future, but the test helped us write our tag today. Because we were forced to think through what defined a successful tag execution, we were able to meet that definition all the more rapidly.


JaVa
   
Comments