◆ Problem
You would like to write Object Tests for a JSP tag handler.
◆ Background
Because the JSP framework invokes the JSP tag handler, it is not obvious how to test the tag handler in isolation. The approach you take depends on whether you want to involve a JSP engine. As we have written previously, there is no mature, standalone JSP engine that you can use outside the context of a web container, so if you want to avoid a JSP engine, your best bet is to simulate it in your test.
Although a little annoying, it only takes a little research to get all the information you need.
◆ Recipe
To test a JSP tag handler in isolation, you must write a test that invokes the tag handler’s methods in the same order that the JSP engine would. Now that sounds like a lot of work, but it is surprisingly simple. Not only that, but you can certainly recuperate the effort you invest in writing this kind of test by refactoring your mini-JSP engine and using it for future work. Perhaps one is available in a publicly available toolkit somewhere.
In order to simulate the JSP engine, at least the part that executes your tag han- dler, we searched the web and found a presentation by Doris Chen of Sun Micro- systems.8 It includes lifecycle graphs for the various kinds of JSP tag handler objects. We used these lifecycle graphs as specifications to build our tests, as they told us the order in which to invoke the various JSP tag handler methods. For our
8 http://developers.sun.com/dev/evangcentral/presentations/customTag.pdf
469 Test a JSP tag handler
example, we consider a custom tag that iterates over the items in a shopcart, pre- senting each shopcart item as a JavaBean that the JSP can then render as it wants.
Listing 12.7 shows you how to use such a tag.
<table name="shopcart" border="1">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Quantity</b></td>
<td><b>Unit Price</b></td>
<td><b>Total Price</b></td>
</tr>
<coffee:eachShopcartItem shopcartBean="shopcartDisplay"
each="item">
<tr>
<td><%= item.coffeeName %></td>
<td id="product-<%= item.productId %>">
<%= item.quantityInKilograms %> kg </td>
<td><%= item.unitPrice %></td>
<td><%= item.getTotalPrice() %></td>
</tr>
</coffee:eachShopcartItem>
<tr>
<td colspan="3"><b>Subtotal</b></td>
<td>
<b><jsp:getProperty name="shopcartDisplay"
property="subtotal" /></b>
</td>
</tr>
</tbody>
</table>
We have highlighted the relevant parts of this JSP fragment in bold print. The tag
<coffee:eachShopcartItem> defines an iterator over the shopcart items, placing each in the scripting variable named by the attribute each. In this case, we named that scripting variable item and use that variable to display a single row. We could certainly further hide the <tr> tag for each shopcart item behind another JSP tag, but that is not germane to the point we want to make here. We see that this as an IterationTag, and so refer to the lifecycle for an IterationTag, which we have translated into pseudocode in listing 12.8. The parts in bold print are the actual method and constant names.
Listing 12.7 Using an IterationTag
470 CHAPTER 12
Testing web components
initialize page context initialize tag attributes whatNext := doStartTag()
if (whatNext == EVAL_BODY_INCLUDE) do
evaluate body
until (doAfterBody() == SKIP_BODY) endif
whatNext := doEndTag() if (whatNext == EVAL_PAGE) evaluate rest of page endif
We need to know the essential strategy behind the tag’s behavior before we know what to test. The tag takes the specified shopcart and iterates over the items in it.
The tag stores each item in a scripting variable—implemented as a page context attribute—so that the JSP can display its properties. We can describe the behavior we need to test, then, in terms of the way we expect the JSP engine to invoke the tag handler.
1 Set the tag attribute values, shopcartBean and each.
2 Invoke doStartTag(). If the shopcart is empty, we should skip the tag body; otherwise, we should store the first shopcart item in the scripting variable named by the attribute each, and then process the body.
3 If the shopcart is not empty, invoke doAfterBody(). If there are more shopcart items, store the next shopcart item in the scripting variable named by the attribute each, then process the body again; otherwise, skip the body.
4 Now that all the shopcart items have been processed, invoke doEndTag(), then evaluate the rest of the page.
We are essentially simulating a JSP engine—a theoretical one that follows the JSP specification correctly. If your vendor supports the specification differently,9 your End-to-End Tests will reveal that, at which point you should feed that information back into your Object Tests with the appropriate comments. This is one case
Listing 12.8 Pseudocode for a generic IterationTag handler
9 A euphemism for “has a defect.”
TE AM FL Y
Team-Fly®
471 Test a JSP tag handler
where comments are certainly appropriate in code: when third-party software does not conform to specifications to which it is meant to conform. We can trans- late these steps into code relatively easily, as in listing 12.9.
package junit.cookbook.coffee.jsp.test;
import java.util.*;
import javax.servlet.jsp.tagext.*;
import junit.framework.*;
import junit.cookbook.coffee.display.*;
import junit.cookbook.coffee.jsp.EachShopcartItemHandler;
import com.diasparsoftware.java.util.Money;
import com.mockobjects.servlet.MockJspWriter;
import com.mockobjects.servlet.MockPageContext;
public class EachShopcartItemHandlerTest extends TestCase { private EachShopcartItemHandler handler;
private ShopcartBean shopcartBean;
private MockPageContext pageContext;
protected void setUp() throws Exception { shopcartBean = new ShopcartBean();
handler = new EachShopcartItemHandler();
pageContext = new MockPageContext() { private Map attributes = new HashMap();
public Object getAttribute(String name) { return attributes.get(name);
}
public void setAttribute(String name, Object value) { attributes.put(name, value);
}
public void removeAttribute(String name) { attributes.remove(name);
} };
MockJspWriter out = new MockJspWriter();
pageContext.setJspWriter(out);
handler.setPageContext(pageContext);
handler.setParent(null);
handler.setShopcartBean(shopcartBean);
handler.setEach("item");
}
Listing 12.9 EachShopcartItemHandlerTest
MockPageContext does not store attributes by default
You can set the expected output and verify it against a String We do not need the parent tag for this test Set the tag attributes
472 CHAPTER 12
Testing web components
public void testEmptyShopcart() throws Exception { assertEquals(Tag.SKIP_BODY, handler.doStartTag());
assertNull(getTheEachAttribute());
assertEquals(Tag.EVAL_PAGE, handler.doEndTag());
}
public void testOneItem() throws Exception {
ShopcartItemBean shopcartItem1 = new ShopcartItemBean(
"Sumatra", "762", 1, Money.dollars(10, 0));
shopcartBean.shopcartItems.add(shopcartItem1);
List shopcartItemAsList = new LinkedList(
shopcartBean.shopcartItems);
assertEquals(Tag.EVAL_BODY_INCLUDE, handler.doStartTag());
assertEquals(shopcartItemAsList.get(0), getTheEachAttribute());
assertEquals(Tag.SKIP_BODY, handler.doAfterBody());
assertNull(getTheEachAttribute());
assertEquals(Tag.EVAL_PAGE, handler.doEndTag());
}
public void testTwoItems() throws Exception { shopcartBean.shopcartItems.add(
new ShopcartItemBean(
"Sumatra", "762", 1, Money.dollars(10, 0)));
shopcartBean.shopcartItems.add(
new ShopcartItemBean(
"Special Blend", "768", 1, Money.dollars(10, 0)));
List shopcartItemAsList = new LinkedList(
shopcartBean.shopcartItems);
assertEquals(Tag.EVAL_BODY_INCLUDE, handler.doStartTag());
assertEquals(shopcartItemAsList.get(0), getTheEachAttribute());
assertEquals(
IterationTag.EVAL_BODY_AGAIN, handler.doAfterBody());
assertEquals(shopcartItemAsList.get(1), getTheEachAttribute());
assertEquals(Tag.SKIP_BODY, handler.doAfterBody());
assertNull(getTheEachAttribute());
assertEquals(Tag.EVAL_PAGE, handler.doEndTag());
}
public Object getTheEachAttribute() { return pageContext.getAttribute("item");
} }
Allows us to refer to each shopcart item by index
A strange name, only because getEachAttribute() could mean something different
473 Test a JSP tag handler
This test shows iterating over an empty shopcart, a single-item shopcart, and a multiple-item shopcart. These are the three distinct cases we need to test, although if it would make you more comfortable, you could test for ten items rather than two. The next step is to turn this into a Parameterized Test Case (see recipe 4.8, “Build a data-driven test suite”) that allows you to test against a variety of values for the tag input attributes.
Notice that we do not test the output of the JspWriter our tag uses. In this case, the tag does not write any output, but simply sets a page context attribute and pro- cesses whatever body it might have. If your tag writes output using the JspWriter, then add these lines of code to the end of your test:
MockJspWriter out = new MockJspWriter();
pageContext.setJspWriter(out);
handler.setPageContext(pageContext);
// The rest of the test
out.setExpectedData("The output you expect");
out.verify();
When you execute the test, the MockJspWriter verifies its actual output against the expected data you specify here. You will use this technique for tags that write directly to the JSP.
◆ Discussion
We have built our own MockPageContext implementation (as an anonymous class) because the version that comes with Mock Objects v0.09 does not store page con- text attributes. The tag we want to test sets those attributes, so we need to add enough behavior to MockPageContext to store, retrieve and remove page context attributes. This is a candidate to move to a reusable library (such as Mock Objects itself). If your tag does not manipulate the page context in this way, then you can use MockPageContext as is. Try it and see.
Compare the logic in each of the three tests with the IterationTag lifecycle to see how the two match up. To help you see what we mean, consider the empty shopcart case. The JSP engine should invoke doStartTag(), which should skip the tag body. The JSP engine should then invoke doEndTag(), which should eval- uate the rest of the page. This case entirely avoids processing the body and invoking doAfterBody().
We do not generally recommend writing your own platform simulators. We strongly recommend against, for example, writing your own servlet processing engine for the sake of writing tests like this. We made an exception here for two key reasons: it turns out to be simpler than we thought and there does not appear
474 CHAPTER 12
Testing web components
to be a viable alternative for writing Object Tests for a JSP tag handler. Our next option would have been to write an End-to-End Test involving a JSP that uses this tag (see recipe 12.3); so we compared the effort of writing this small JSP tag pro- cessor, executing and maintaining the corresponding tests against writing, and executing and maintaining the corresponding End-to-End Tests. We judged that it was worth taking an hour or so to learn how to write these tests. We were right this time. As always, be aware of the alternatives and the overall cost of each option before you make your choice. If you do not know, then ask; the JUnit com- munity is only too happy to help you.
◆ Related
■ 4.8—Build a data-driven test suite
■ 12.3—Test rendering a JavaServer Page