◆ Problem
You want to test the data passed into a page template without having to involve the web component that forwards to that page template. The page template could be a JSP, Velocity template, or some other page template mechanism.
20A Java-based HTTP server that is easy to embed in applications. Vincent Massol and Ted Husted describe using Jetty in JUnit in Action (Manning, 2003) and you can find the project at http://jetty.mortbay.org/jetty/.
496 CHAPTER 12
Testing web components
◆ Background
In many ways, a page template is only as good as the data you pass into it. It is eas- iest, and therefore most common, to test passing data to a page template by ren- dering the page and inspecting it. When errors occur, the page often dies in some spectacular manner, and as a result you might fall into the trap of seeing the page templates as the problem, rather than the rest of the application around it. As you investigate defects in the application, if you notice that the page template itself is not often to blame, then you might want to write tests to verify the data that you passed into them.
◆ Recipe
Your JSPs use data to present dynamic content to the end user. You find this dynamic data in as many as five sources: the HTTP request, the HTTP session, the page itself, the application context, or an external data source from which the page template pulls data.
The first four of these data sources21 are the familiar servlet API objects: request, response, page, and application, and have one thing in common: the servlet places data into these objects, and then the page template pulls the data out of these objects. They represent a kind of shared memory space between the Con- troller and the View. The general strategy to test this interaction is to verify that the Controller supplies the correct data for a given request.
The last of these five data sources is most commonly a JDBC data source—the page template either includes JDBC code directly or uses a data-aware component such as an Enterprise JavaBean or Data Bean. With this design, the Controller provides primary key information to the page template, which then pulls the rest of the data from “the database” using that primary key information. In addition to verifying that the Controller supplies the correct primary key data, you need to test that the page template invokes the data-aware component properly.
In either case, you certainly need to test the data retrieval code separately, and we invite you to refer to chapter 10, “Testing and JDBC,” for details.
We now return to the Coffee Shop application and consider the shopcart dis- play JSP. Listing 12.18 shows the page template.
21We do not mean J2EE Data Sources, such as relational databases. We just mean “sources of data.”
497 Verify the data passed
to a page template
<%@page import="java.util.*" %>
<%@page import="junit.cookbook.coffee.display.*" %>
<jsp:useBean id="shopcartDisplay"
class="junit.cookbook.coffee.display.ShopcartBean"
scope="request" />
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head><title>Your Coffee Shop Shopcart</title></head>
<body>
<h1>Your Shopcart Contains</h1>
<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>
<%
for (Iterator i = shopcartDisplay.shopcartItems.iterator();
i.hasNext(); ) {
ShopcartItemBean item = (ShopcartItemBean) i.next();
%>
<tr>
<td><%= item.coffeeName %></td>
<td id="product-<%= item.productId %>">
<%= item.quantityInKilograms %> kg </td>
<td><%= item.unitPrice %></td>
<td><%= item.getTotalPrice() %></td>
</tr>
<%
}
%>
<tr>
<td colspan="3"><b>Subtotal</b></td>
<td><b><%= shopcartDisplay.getSubtotal() %></b></td>
</tr>
</tbody>
</table>
<form action="coffee" method="POST">
<input type="submit"
name="browseCatalog" value="Buy More Coffee!" />
</form>
</body>
</html>
Listing 12.18 Shopcart Display page template (JSP)
498 CHAPTER 12
Testing web components
This JSP expects a ShopcartBean containing ShopcartBeanItems and displays a sub- total for the items in the shopcart. You compute shipping and taxes when the user submits the order for processing. What do we need to test, then? The JSP expects to see a ShopcartBean in the HTTP request as an attribute with the name shop- cartDisplay. Those are the assertions for our first test.
public void testControllerProvidesShopcartBean() { // This does not yet compile
// Arrange?
// Act?
Object shopcartDisplayAttribute =
request.getAttribute("shopcartDisplay");
assertNotNull(shopcartDisplayAttribute);
assertTrue(shopcartDisplayAttribute instanceof ShopcartBean);
}
We need more code here. Specifically, we need to initialize our test environment and request the shopcart page. It seems reasonable to start with a user who does not yet have a shopcart, then verify that the act of requesting the shopcart places an empty shopcart in the request. If the Controller does its job—and that is what we plan to test here—then we can separately test that the JSP displaying the shop- cart does indeed display the data as expected. As for checking the layout, nothing beats good, old-fashioned visual inspection. There are some things you just have to see before you can be confident that they are right.
As for the rest of this code, you have two choices, depending on whether you have access to the source code of the Controller. If you need to test the Controller as is, then you need to send an HTTP request to the servlet and verify the contents of the request after invoking the service() method. For this, use ServletUnit. List- ing 12.19 shows an example.
public void testControllerProvidesShopcartBean() throws Exception { ServletRunner servletRunner = new ServletRunner();
servletRunner.registerServlet(
"CoffeeShopController",
CoffeeShopController.class.getName());
CoffeeShopController coffeeShopController = new CoffeeShopController() {
public void log(String message) { // Intentionally disable logging }
};
Listing 12.19 testControllerProvidesShopcartBean()
499 Verify the data passed
to a page template
ServletUnitClient client = servletRunner.newClient();
WebRequest addToShopcartRequest = new PostMethodWebRequest(
"http://localhost:9080/coffeeShop/coffee");
addToShopcartRequest.setParameter("displayShopcart", "shopcart");
InvocationContext invocationContext =
client.newInvocation(addToShopcartRequest);
coffeeShopController.handleDisplayShopcart(
invocationContext.getRequest());
Object shopcartDisplayAttribute =
invocationContext.getRequest().getAttribute("shopcartDisplay");
assertNotNull(shopcartDisplayAttribute);
assertTrue(shopcartDisplayAttribute instanceof ShopcartBean);
}
The “arrange” part of this test involves the usual ServletUnit set up: creating a ServletRunner, registering the servlet and creating a client.22 The “act” part of this test involves creating the “display shopcart” request, creating the invocation con- text (to convert the WebRequest into an HttpServletRequest) and invoking the request handler. Notice that we do not invoke doPost(), but just invoke the correct handler for the request. If we try to invoke doPost(), ServletUnit throws a NullPointerException deep inside. That should not worry you: after all, you want to test the request handler, and not the rest of the servlet. If the request handler puts the right data in the request, then the JSP works as expected. That is the point of this test. We extracted the method handleDisplayShopcart()23 and made it pub- lic in order to invoke it for this test. If you prefer, leave handleDisplayShopcart() protected and put your test in the same package, but a different source tree. (See recipe 3.3, “Separate test packages from production code packages,” for details.)
◆ Discussion
This test does not need to change at all if you use Velocity templates as your pres- entation layer. Because the test prepares the data to be displayed but does not actually invoke the presentation layer, it does not matter what presentation layer
22There is a clue in that last sentence—perhaps we need to extract this into a test fixture. See recipe 3.4,
“Factor out a test fixture.”
23This method has since been refactored out of the class, so if you look at the code online, you will not see it. Do not worry about that, because it is the concept, and not the code, that matters here.
500 CHAPTER 12
Testing web components
you use: forward to a JSP or merge with a Velocity template. Any way you do it, the test remains the same. You can even use this technique to verify data passed to any component, such as a web resource filter. (See recipe 12.13, “Test a web resource filter,” for a total strategy for testing a filter.)
As a general design rule, we recommend that page templates not pull data from a data source, but rather display the data the Controller sends to it. The Controller ought to retrieve all the data to be displayed, and then pass it to the page template through session attributes, request attributes, or template context, whatever the appropriate underlying mechanism. This design reduces the cou- pling between your presentation layer and the business layer, allowing you to sub- stitute a different presentation layer when a new technically minded manager joins your project and declares, “From this point forward, we use Velocity tem- plates rather than JSPs!” You might doubt that, but it happens.
◆ Related
■ 3.3—Separate test packages from production code packages
■ 3.4—Factor out a test fixture