12.3 Test rendering a JavaServer Page
◆ Problem
You want to verify the output of a JavaServer Page.
457 Test rendering a JavaServer Page
◆ Background
Testing JSPs in isolation—that is, without simply writing End-to-End Tests—is one of those activities that many people find too difficult to be worth the effort. We find that strange, especially in light of the way many people write JSPs in the first place. It is commonplace for a web author to start with a static web page contain- ing dummy data. This makes it easy to work on both layout and general look-and- feel using web authoring tools such as Dreamweaver. After the page looks good, it is time to replace the static content with placeholders for dynamic content which the application will provide. These placeholders correspond to JavaBean proper- ties, so now the “only way” to see the rendered JSP is to get real data from the application, which is most easily done by executing the application end to end.
◆ Recipe
Rather than test your application from end to end, we recommend hard coding some data for the JSP, then rendering it directly using a JSP engine. You can com- pare the JSP engine’s output with a Gold Master—a version of the JSP output that you have checked by hand once and then filed away as “correct.”
The general strategy is to use ServletUnit along with Jasper4 to render the JSP in question. ServletUnit also allows you to intercept the request on the way to the JSP so that you can add data to it in the form of request or session attributes. Finally, you will apply the Gold Master technique, comparing the current JSP output to known, correct output.5
The example that follows consists of a fair amount of code, so we will explore it in pieces. Much of this code is reusable, and so represents a one-time effort, leav- ing surprisingly little code to write for the dozens of tests you need to test all your JSPs. In listing 12.3 we start with the “easy” part: the tests themselves.
package junit.cookbook.jsp.test;
import java.io.File;
import javax.servlet.http.HttpServletRequest;
import junit.cookbook.coffee.display.*;
import junit.cookbook.coffee.presentation.test.JspTestCase;
4 Here we are using the Apache Tomcat web container, which includes Jasper as its JSP engine.
5 We discuss the Gold Master technique in recipe 10.2, “Verify your SQL commands.”
Listing 12.3 RenderShopcartJspTest
458 CHAPTER 12
Testing web components
import com.diasparsoftware.java.util.Money;
import com.diasparsoftware.javax.servlet.ForwardingServlet;
import com.diasparsoftware.util.junit.GoldMasterFile;
import com.meterware.servletunit.*;
public class RenderShopcartJspTest extends JspTestCase { private ShopcartBean shopcartBean;
private ServletRunner servletRunner;
private ServletUnitClient client;
protected void setUp() throws Exception { shopcartBean = new ShopcartBean();
servletRunner = new ServletRunner(
getWebContentPath("/WEB-INF/web.xml"), "/coffeeShop");
servletRunner.registerServlet(
"/forward",
ForwardingServlet.class.getName());
client = servletRunner.newClient();
}
public void testEmptyShopcart() throws Exception { checkShopcartPageAgainst(
new File(
"test/gold",
"emptyShopcart-master.txt"));
}
public void testOneItemInShopcart() throws Exception { shopcartBean.shopcartItems.add(
new ShopcartItemBean(
"Sumatra", "762", 5,
Money.dollars(7, 50)));
checkShopcartPageAgainst(
new File(
"test/gold",
"oneItemInShopcart-master.txt"));
}
// Helper code omitted for now }
The superclass JspTestCase provides some useful methods for locating JSPs on the file system and deciding where on the file system the JSP engine should generate servlet source code. If you are interested in the details, see the Discussion
JspTestCase contains some convenience methods
Register an entire Web Deployment Descriptor
A dummy servlet to help serve up JSPs
Check against the Gold Master
459 Test rendering a JavaServer Page
section of this recipe, but we recommend reading on first, and then coming back to the details when the rest of this recipe is in focus.
The tests are tiny: add some items to a shopcart (or not, in the case of the empty shopcart case) then check the resulting page against a Gold Master. This method
—checkShopcartPageAgainst()—is where all the magic happens, but before we get to that, we first look at ForwardingServlet. This is a simple servlet that does two things: lets a test put data into a request (or session) and forwards the request to the URI we specify. This simulates what our CoffeeShopController does in produc- tion after all the business logic and database updates are complete. Our strategy here is to eliminate the business logic because what we want has nothing to do with business logic: we simply want to verify that a JSP “looks right.” We write the For- wardingServlet once—or hope that someone else provides one for us6—then use it for the rest of these kinds of tests. Listing 12.4 shows the result.
package com.diasparsoftware.javax.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class ForwardingServlet extends HttpServlet { private String forwardUri = "";
protected void doGet(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { handleRequest(request, response);
}
protected void doPost(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { handleRequest(request, response);
}
protected void handleRequest(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
6 Diasparsoft Toolkit (www.diasparsoftware.com/toolkit) includes ForwardingServlet for use in tests.
Listing 12.4 ForwardingServlet
460 CHAPTER 12
Testing web components
getServletContext().getRequestDispatcher(
getForwardUri()).forward(
request, response);
}
public void setForwardUri(String forwardUri) { this.forwardUri = forwardUri;
} public String getForwardUri() {
return forwardUri;
} }
The next part is rendering the JSP and retrieving its content—that is, using the For- wardingServlet in combination with ServletUnit, then reading the JSP output as text.
public String getActualShopcartPageContent() throws Exception {
InvocationContext invocationContext = client.newInvocation(
"http://localhost/coffeeShop/forward");
ForwardingServlet servlet =
(ForwardingServlet) invocationContext.getServlet();
servlet.setForwardUri("/shopcart.jsp");
HttpServletRequest request = invocationContext.getRequest();
request.setAttribute("shopcartDisplay", shopcartBean);
servlet.service(request, invocationContext.getResponse());
return invocationContext.getServletResponse().getText();
}
This is a direct translation of the steps we needed to test the JSP: put data in the request, render the JSP, and look at the resulting web page. Notice that we do not worry about where the data comes from—we just hard code the data we want to display and stuff it into the request, where the JSP expects it to be. We know that the shopcart data comes from the user’s session object and we know that we have to translate that session object into a ShopcartBean, but we do not care about those details for this test. Tomorrow, when it turns out we need to store shopcart data in the database and retrieve it using a ShopcartStore (see chapter 10, “Testing and JDBC”), this test remains unaffected. That is one indicator of a good design: no ripple effect. Good work!
Specify which URI to forward to when invoking service()
We created the ServletUnitClient in setUp()
Put the shopcart data on the request
Invoke the Forwarding- Servlet
Get the JSP output as text
TE AM FL Y
Team-Fly®
461 Test rendering a JavaServer Page
The last piece of the puzzle comes in two parts: the Gold Master. We say two parts because to use the Gold Master technique requires first creating the Gold Master and checking it by visual inspection, then verifying future output against that Gold Master. To create the Gold Master you need to write the JSP text out to a file:
public void generateGoldMaster(File goldMasterFile) throws Exception {
String responseText = getActualShopcartPageContent();
new GoldMasterFile(goldMasterFile).write(responseText);
fail("Writing Gold Master file.");
}
When you first code your test, have it invoke generateGoldMaster(). This method creates the Gold Master file and fails the test as a reminder that you have not fin- ished yet. This last point is important. If you let the test pass it is possible for some- one to run the test, believe it is actually testing something, and not realize that there is work to do. You can ignore a passing test, but not a failing test!7 So your test will look like this the first time you execute it:
public void testEmptyShopcart() throws Exception { generateGoldMaster(
new File(
"test/gold",
"emptyShopcart-master.txt"));
}
Execute the test, then inspect the output yourself:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=ISO-8859-1" />
<meta name="GENERATOR" content="IBM WebSphere Studio" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<link href="theme/Master.css" rel="stylesheet" type="text/css" />
<title>shopcart.jsp</title>
</head>
<body>
<h1>your shopcart contains</h1>
<table name="shopcart" border="1">
<tbody>
<tr>
<td><b>Name</b></td>
7 Some people might use this opportunity to “ignore” the test (see recipe 6.6, “Ignore a test”) rather than have it fail. We recommend the latter, but it is largely a question of personal taste. Do what works for you.
462 CHAPTER 12
Testing web components
<td><b>Quantity</b></td>
<td><b>Unit Price</b></td>
<td><b>Total Price</b></td>
</tr>
<tr>
<td colspan="3"><b>Subtotal</b></td>
<td><b>$0.00</b></td>
</tr>
</tbody>
</table>
<form action="coffee" method="POST"><input type="submit"
name="browseCatalog" value="Buy More Coffee!" /></form>
</body>
</html>
This looks right: displaying an empty shopcart means no items in the cart and a zero subtotal. There is your Gold Master file. Now that you have written it to disk, change the test so that it checks the response from the server against the Gold Master:
public void testEmptyShopcart() throws Exception { checkShopcartPageAgainst(
new File(
"test/gold",
"emptyShopcart-master.txt"));
}
such that checkShopcartPageAgainst() looks like this:
public void checkShopcartPageAgainst(File goldMasterFile) throws Exception {
String responseText = getActualShopcartPageContent();
new GoldMasterFile(goldMasterFile).check(
responseText);
}
The class com.diasparsoftware.util.junit.GoldMasterFile is also part of Dias- parsoft Toolkit and provides convenience methods for generating and checking against a Gold Master. Listing 12.5 shows the source for this class.
package com.diasparsoftware.util.junit;
import java.io.*;
import junit.framework.Assert;
public class GoldMasterFile extends Assert { private File file;
public GoldMasterFile(String directory, String file) { Listing 12.5 GoldMasterFile
463 Test rendering a JavaServer Page
this(new File(directory, file));
}
public GoldMasterFile(File file) { this.file = file;
}
public void write(String content) throws IOException { file.getParentFile().mkdirs();
FileWriter goldMasterWriter = new FileWriter(file);
goldMasterWriter.write(content);
goldMasterWriter.close();
}
public void check(String actualContent) throws IOException {
assertTrue(
"Gold master [" + file.getAbsolutePath() + "] not found.", file.exists());
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
BufferedReader goldMasterReader =
new BufferedReader(new FileReader(file));
while (true) {
String line = goldMasterReader.readLine();
if (line == null) break;
printWriter.println(line);
}
assertEquals(stringWriter.toString(), actualContent);
} }
So that is everything: to write a new test, simply add items to the shopcart, write the JSP output to disk, inspect the results by hand, make it a Gold Master, then change the test to check against that Gold Master. If the test ever fails—and it will whenever you change the JSP—just inspect the Gold Master by hand again.
◆ Discussion
We tried to find a standalone JSP engine that we could use to execute these tests and came up empty. We did try to use Jasper—the JSP engine embedded in Apache Tomcat—but we had to write a considerable amount of code, mostly fak- ing context objects, just to get to the point where we could compile a JSP, let alone execute it. For that reason we abandoned this approach, preferring instead to use
464 CHAPTER 12
Testing web components
an actual container to process the JSPs. Perhaps by the time you read this there will be a standalone JSP engine that you can use in its place. If so, you can compile and execute the JSP directly rather than going through a web container. So much the better.
Test performance is always an issue to consider, so we feel it is important to mention the cost of executing these tests. In terms of time expense, the cost to execute these tests is approximately 2 seconds of startup cost and 2 seconds per test. These numbers are based on a P4-2.4 GHz machine with all operations occur- ring in memory (no swapping). This means that a site with 100 JSPs might need a total of 10 minutes (3 test scenarios per JSP times 100 JSPs, times 2 seconds each) to execute an exhaustive test suite for its JSPs. We recommend that you use a back- ground-running continual build system such as Cruise Control to execute these tests regularly, rather than trying to make them part of the Programmer Test suite that you execute whenever you change the production code. The bulk of the cost comes from rendering the JSP and comparing the resulting web page against con- tent we retrieve from disk. In order to speed up these tests, we need to eliminate these costly operations.
We can verify that the JSP has the correct data to display, but that has nothing to do with the JSP. Instead, see recipe 12.12, “Verify the data passed to a page tem- plate,” which, in spite of its title, only tests the servlet.
We can verify that the JSP displays the correct data without worrying about lay- out or look and feel by using XMLUnit. See chapter 9, “Testing and XML,” for rec- ipes involving XMLUnit.
For the sake of completeness, here is the code for JspTestCase:
public abstract class JspTestCase extends TestCase { protected static final String webApplicationRoot = "../CoffeeShopWeb/Web Content"; | #1
protected String getWebContentPath(String relativePath) { return new File(
webApplicationRoot, relativePath).getAbsolutePath();
}
protected String getCoffeeShopUrlString(String uri) throws Exception {
return "http://localhost/coffeeShop" + uri;
}
protected String getJspTempDirectory() {
return System.getProperty("java.io.tmpdir");
}
Change these values for your project
465 Test rendering a Velocity template
protected void tearDown() throws Exception { File[] files =
new File(getJspTempDirectory()).listFiles(
new FilenameFilter() {
public boolean accept(File dir, String name) { return name.endsWith(".java")
|| name.endsWith(".class");
} });
for (int i = 0; i < files.length; i++) { File file = files[i];
file.delete();
} } }
One final note: we do not recommend using the Gold Master technique for out- put that constantly changes. This technique is best used to detect inadvertent or unexpected changes in output. If cosmetic changes such as look-and-feel or layout enhancements are likely to happen, we strongly recommend that you verify just the dynamic content parts of the JSP, as in the Discussion section of recipe 12.10,
“Verify web page content without a web server.” Those tests tend to be much less brittle than tests that use the Gold Master technique.
◆ Related