12.13 Test a web resource filter
◆ Problem
You want to test a web resource filter.
◆ Background
Filters can make a web application difficult to understand. Runtime behavior appears as if by magic, because you do not see that behavior in the servlet, the most logical place to start looking. Moreover, debugging a filter can be quite annoying:
typically you end up executing manual, End-to-End Tests with a browser, and log- ging information to the web application log. This is very time consuming, and one instance where adding a few tests can save hours of headache.
◆ Recipe
A filter has two responsibilities: execute certain logic, and do it at the appropriate time. That is, not only do you need to test what the filter does, but you need to test when it does that work in relation to when it invokes the rest of the filter chain.
Testing the logic is the easy part, so let us start with that.
TE AM FL Y
Team-Fly®
501 Test a web resource filter
As with any other framework component, a filter consists of two parts: its logic and its integration to the framework. In general, you should separate those two parts, refactoring the logic to a separate class where possible, leaving behind in the Filter implementation only its integration to the web container. For all but the simplest filters, we recommend performing this refactoring. We describe this technique in recipe 12.1 as well as throughout chapter 11, “Testing Enterprise Java- Beans.” It so happens that the filter we use as an example is simple enough that we can take a shortcut.
Our Coffee Shop application contains an annoying bit of behavior. Due to the way the HTTP session API behaves, our CoffeeShopController needs to verify on every request whether the user has a session and, if so, whether that session con- tains a ShopcartModel object. It would simplify the servlet if we could extract that check to a filter. Not only does it make the servlet easier to understand, but also easier to test: for tests that have nothing to do with session data, there is no need to mock up the method HttpServletRequest.getSession(). The less we need to do, the better. Our filter, then, ensures that the user has a shopcart in her session.
As such, we call it EnsureShopperHasShopcartFilter.
The logic our filter executes, then, can be summarized as follows: create a ses- sion, if needed, and if it has no shopcart, add an empty one. Because we have no reason to do otherwise, we decide to execute this logic before invoking the rest of the fil- ter chain. There are essentially three tests that we need to pass:
1 If the user has no session to start with, then after invoking the filter, she has a session and a shopcart.
2 If the user has a session without a shopcart to start with, then after invok- ing the filter, she has a session with a shopcart.
3 If the user already has a shopcart, the filter does not change the session in any way.
This appears to be sufficient to test the filter’s logic. We can easily implement this in a single method, so we decide to implement it directly on the filter class and make it public, rather than extracting it to a separate class as we recommend doing in general. This is one of those make-as-you-go trade-offs: we cannot really reuse this logic outside the context of a web application, anyway, so moving it to a separate class does not appear to make sense yet. Perhaps something else—a new feature or a design change elsewhere—will change the balance of pros and cons here. We will take that as it comes. Listing 12.20 shows the three tests.
502 CHAPTER 12
Testing web components
package junit.cookbook.coffee.test;
import junit.cookbook.coffee.EnsureShopperHasShopcartFilter;
import junit.cookbook.coffee.model.ShopcartModel;
import junit.framework.TestCase;
import com.diasparsoftware.javax.servlet.http.HttpSessionAdapter;
import com.mockobjects.servlet.*;
public class EnsureShopperHasShopcartFilterLogicTest extends TestCase {
private FakeHttpSession session;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private EnsureShopperHasShopcartFilter filter;
protected void setUp() throws Exception { session = new FakeHttpSession();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
filter = new EnsureShopperHasShopcartFilter();
request.setSession(session);
}
public void testAlreadyHasShopcart() throws Exception { ShopcartModel shopcartModel = new ShopcartModel();
session.setAttribute("shopcartModel", shopcartModel);
filter.addShopcartIfNeeded(request);
assertSame(
shopcartModel,
request.getSession(true).getAttribute("shopcartModel"));
}
public void testEmptySession() throws Exception { filter.addShopcartIfNeeded(request);
assertNotNull(session.shopcartModelAttribute);
assertTrue(session.shopcartModelAttribute.isEmpty());
}
public void testNoSession() throws Exception { request.setExpectedCreateSession(true);
filter.addShopcartIfNeeded(request);
assertNotNull(session.shopcartModelAttribute);
assertTrue(session.shopcartModelAttribute.isEmpty());
}
public static class FakeHttpSession extends HttpSessionAdapter { public ShopcartModel shopcartModelAttribute;
Listing 12.20 EnsureShopperHasShopcartFilterLogicTest
503 Test a web resource filter
public Object getAttribute(String name) { return shopcartModelAttribute;
}
public void setAttribute(String name, Object value) { shopcartModelAttribute = (ShopcartModel) value;
} } }
Notice the use of assertSame() in the “already has shopcart” test. If the user already has a shopcart in her session, we want the filter to leave that shopcart alone. To implement this assertion, we invoke the filter logic, then verify that the session contains the same shopcart object that it had before invoking the filter logic, and not just an equivalent one. We could take this test one step further: clone the
“before” shopcart, apply the filter logic, assert that the session has the same shop- cart object, then assert that the “after” shopcart is equal to (contains the same items as) the “before” shopcart. If you want to be certain that the filter logic does not place 50 items in the shopcart, then this is a good idea. It all depends on the level of confidence you need in the correctness of that logic. Next, we need to test that the filter invokes its logic before invoking the filter chain.
This test is more complex, requiring a more complex fixture, so we create a separate one. We need to test when the method doFilter() invokes addShopcart- IfNeeded() and filterChain.doFilter(), so we need to be a little sneaky here.
The simplest solution we imagined involves doing something we ordinarily dis- like: subclassing the class under test. The idea is to override the method addShop- cartIfNeeded() and store in a flag if the method has been invoked. We then create a mock FilterChain that fails when its method doFilter() is invoked when this flag is false. Listing 12.21 shows the test, with the “sneaky parts” highlighted in bold print.
package junit.cookbook.coffee.test;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpSession;
import junit.cookbook.coffee.EnsureShopperHasShopcartFilter;
import junit.framework.TestCase;
import com.diasparsoftware.javax.servlet.http.HttpSessionAdapter;
import com.mockobjects.servlet.*;
Listing 12.21 EnsureShopperHasShopcartFilterIntegrationTest
504 CHAPTER 12
Testing web components
public class EnsureShopperHasShopcartFilterIntegrationTest extends TestCase {
private HttpSession session;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private MockFilterChain filterChain;
private EnsureShopperHasShopcartFilter filter;
private boolean invokedFilterChainDoFilter;
private boolean filterLogicDone;
protected void setUp() throws Exception { filterLogicDone = false;
invokedFilterChainDoFilter = false;
session = new HttpSessionAdapter();
request = new MockHttpServletRequest();
request.setSession(session);
response = new MockHttpServletResponse();
filterChain = new MockFilterChain() { public void doFilter(
ServletRequest request, ServletResponse response)
throws IOException, ServletException { assertTrue(
"Something invoked filterChain.doFilter "
+ "before the filter logic was done", filterLogicDone);
invokedFilterChainDoFilter = true;
} };
filter = new EnsureShopperHasShopcartFilter() {
public void addShopcartIfNeeded(ServletRequest request) { super.addShopcartIfNeeded(request);
filterLogicDone = true;
} };
}
public void testInvokesFilterChain() throws Exception { filter.doFilter(request, response, filterChain);
assertTrue(invokedFilterChainDoFilter);
} }
If we needed to invoke the filter chain before the filter logic, we would use the same technique, but in reverse: FilterChain.doFilter() would have to set the “someone
505 Test a web resource filter
invoked me” flag, then we would override addShopcartIfNeeded() to assert that that flag had been set. Now that we have tested the filter logic and its integration with the filter chain, what remains is to test that it has been correctly deployed.
It is important to verify that your web application actually invokes the filter;
otherwise the rest of your testing effort goes for naught. One approach is to test the servlet in a live container, another is to use ServletUnit, but these approaches have a common weakness: they test the web container, rather than the informa- tion you provide to the web container. Don’t test the platform. Instead, use XMLUnit to verify that you have specified the deployment descriptor correctly, a technique we describe in detail in the introduction to chapter 9, “Testing and XML.”
Listing 12.22 shows a few simple tests.
package junit.cookbook.coffee.deployment.test;
import java.io.*;
import junit.cookbook.coffee.EnsureShopperHasShopcartFilter;
import org.custommonkey.xmlunit.*;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
public class FiltersTest extends XMLTestCase {
public void testEnsureShopperHasShopcartFilterConfigured() throws Exception {
String webDeploymentDescriptorFilename =
"../CoffeeShopWeb/Web Content/WEB-INF/web.xml";
Document webDeploymentDescriptorDocument = XMLUnit.buildTestDocument(
new InputSource(
new FileReader(
new File(webDeploymentDescriptorFilename))));
String filterNameMatch =
"[filter-name='EnsureShopperHasShopcartFilter']";
assertXpathExists(
"/web-app/filter" + filterNameMatch, webDeploymentDescriptorDocument);
assertXpathEvaluatesTo(
EnsureShopperHasShopcartFilter.class.getName(), "/web-app/filter" + filterNameMatch + "/filter-class", webDeploymentDescriptorDocument);
assertXpathExists(
"/web-app/filter-mapping" + filterNameMatch, webDeploymentDescriptorDocument);
Listing 12.22 Some simple filter tests
506 CHAPTER 12
Testing web components
assertXpathEvaluatesTo(
"/coffee",
"/web-app/filter-mapping"
+ filterNameMatch + "/url-pattern",
webDeploymentDescriptorDocument);
} }
The first assertion verifies that a filter is configured at all; the next verifies the class name implementing the filter. The third assertion verifies that the filter is mapped to some URL; the next verifies the URL to which it is mapped. This is the beginning of a general pattern for testing the deployment of web resource filters and probably ought to be refactored into its own class. At a minimum, this test is an excellent candidate to refactor towards a Parameterized Test Case (see recipe 4.8,
“Build a data-driven test suite”); so if we were to add more tests, we would likely perform the refactoring.
Finally, for the sake of completeness, listing 12.23 shows a filter that passes these tests.
package junit.cookbook.coffee;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import junit.cookbook.coffee.model.ShopcartModel;
public class EnsureShopperHasShopcartFilter implements Filter { public void doFilter(
ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException { addShopcartIfNeeded(request);
filterChain.doFilter(request, response);
}
public void addShopcartIfNeeded(ServletRequest request) { HttpSession session =
((HttpServletRequest) request).getSession(true);
ShopcartModel shopcartModel =
(ShopcartModel) session.getAttribute("shopcartModel");
Listing 12.23 EnsureShopperHasShopcartFilter (final version)
507 Test a web resource filter
if (shopcartModel == null) {
session.setAttribute("shopcartModel", new ShopcartModel());
} }
public void init(FilterConfig config) { }
public void destroy() { }
}
◆ Discussion
If we were really concerned about subclassing the class under test—and ordinarily we are—we could easily have extracted the filter logic to a separate class. In this case, for the above test we would substitute a Spy implementation of the filter logic that kept track of whether it had yet been invoked. As we wrote previously, this example is simple enough that the risk of subclassing the class under test is low. It is worth noting, however, that the desire to subclass the class under test tends to indicate a need to perform the refactoring Extract Class [Refactoring, 149]. You will typically move the methods you override into the new class.
If your web resource filter requires information from the outside world, such as input from a servlet, then use the techniques in recipe 12.12. In this case, the web resource filter plays the same role that a page template plays: it is the recipient of data that some component (the servlet) has passed into the request object.
◆ Related
■ 4.8—Build a data-driven test suite
■ Chapter 9—Testing and XML
■ Chapter 11—Testing Enterprise Java Beans
■ 12.1—Test updating session data without a container
■ 12.12—Verify the data passed to a page template
508
Testing J2EE applications
This chapter covers
■ Testing web application page flow, including Struts
■ Testing your site for broken links
■ Testing web and EJB resource security
■ Testing container-managed transactions
509 Testing J2EE applications
As you read this book it should become clear that we advocate testing an application by testing its components thoroughly, and then integrating those components as simply as possible. Specifically, “integration” for us is little more than choosing which implementations of various interfaces to use, and then creating an applica- tion entry point object with references to those implementations. Which logging strategy do we use? How about Log4J! We know that our components work with any implementation of the logging strategy interface. What kind of model? A JDBC- based one, although our Controller really only knows about our Model interface, so an in-memory implementation, or one based on Prevayler (www.prevayler.org) will do. To us, this is integration. As a result, we tend not to emphasize End-to-End Test- ing for correctness, but rather to give us confidence that we have built the features we needed to build. Object Tests tell you whether you built the thing right; whereas End-to-End Tests help you decide whether you built the right thing.
There are certain aspects of J2EE applications that people associate with End-to- End Tests rather than Object Tests. These include page flow—or navigating a web application—and using container services, such as security and transactions. We discuss these topics in recipes in this chapter, showing you how to test these behav- iors in isolation—as Object Tests. We do not want to give the impression that we shy away from End-to-End Tests—that is, testing an application by simulating the way an end user interacts with it through its end-user interface. As we wrote previ- ously, we use End-to-End Tests to play a different role than other programmers do: we use End-to-End Tests to help us determine whether what we have built actu- ally does what our customers need. We do discuss using HtmlUnit to write End-to- End Tests for a web application (see recipe 13.1), but we no longer see JUnit as the best tool available for testing an application from end to end. We use Fit (http://fit.c2.com) and its companion tool, FitNesse (www.fitnesse.org).
NOTE We are certainly not the only people who see End-to-End Tests in this role: we got the idea from the Agile community (www.agilealliance.org) at large. Still, while Agile developers remain in the minority, organiza- tions will continue to see End-to-End Tests as their primary tool for vali- dating software, an approach that we feel ultimately wastes resources that could be better spent ensuring correctness from the inside out through Programmer Tests.
Because this is a book on JUnit, and not Fit, we will describe Fit only briefly. Imag- ine writing tests entirely as spreadsheets and word-processor documents. You could annotate your tests with plain-language descriptions of what they verify—
mix code and text together so that both the programmers and the nonprogram- mers can follow them. Now imagine running those tests through software that
510 CHAPTER 13
Testing J2EE applications
decorates your documents green for the parts that are right (tests pass) and red for the parts that are wrong (tests fail). That is Fit, and it allows those with busi- ness knowledge to write executable tests, even if they are not programmers. Fit- Nesse1 is a Wiki (www.wiki.org) that can execute Fit tests, providing an excellent way to organize them and collaborate on them. Many people have designated Fit- Nesse as “the way they do End-to-End Tests.” We are among them.
But this is a book about JUnit, and this is a chapter on writing Object Tests for aspects of J2EE applications that one usually tests from end to end. In here is a collec- tion of recipes that will help you test certain aspects of J2EE applications more effec- tively. These are behaviors that tend to be sprinkled throughout the application:
page flow, broken links, security, transactions, and JNDI. We have added a recipe related to the Struts application framework (http://jakarta.apache.org/struts), but for more on testing Struts applications, we recommend the StrutsTestCase project (http://strutstestcase.sourceforge.net). It provides both a mock objects approach (testing outside the application server) and a Cactus approach (testing inside the application server). It embodies many of the techniques we have described in this book, and rather than duplicate their fine work, we refer you to them.