Testing J2EE applications

Recipes for testing enterprise apps

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 application 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 (Java Database Connectivity) one, although our controller really only knows about our model interface, so an in-memory implementation, or one based on Prevayler will do. To us, this is integration. As a result, we tend not to emphasize end-to-end testing 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 behaviors 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. 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 actually does what our customers need. We no longer see JUnit as the best tool available for testing an application from end to end. We use Fit and its companion tool, FitNesse.

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 at large. Still, while Agile developers remain in the minority, organizations will continue to see end-to-end tests as their primary tool for validating software, an approach that we feel ultimately wastes resources that could be better spent ensuring correctness from the inside out through programmer tests.

We will describe Fit only briefly. Imagine 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 nonprogrammers can follow them. Now imagine running those tests through software that 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 business knowledge to write executable tests, even if they are not programmers. FitNesse is a Wiki that can execute Fit tests, providing an excellent way to organize them and collaborate on them. Many people have designated FitNesse as "the way they do end-to-end tests." (Pronounced "fit-NESS." Micah Martin, cocreator of FitNesse, tells the story how Uncle Bob (Robert C. Martin) was tired of executing Fit tests from the command line and wanted to execute them "with finesse.")

But 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 collection of recipes that will help you test certain aspects of J2EE applications more effectively. These are behaviors that tend to be sprinkled throughout the application: page flow, broken links, security, transactions, and JNDI (Java Naming and Directory Interface). We have added a recipe related to the Struts application framework, but for more on testing Struts applications, we recommend the StrutsTestCase project. 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, and, rather than duplicate their fine work, we refer you to them.

Test page flow

Problem

You want to verify that a user can navigate through your Web application's pages correctly, and you want to make the verification without involving all the machinery of the application itself.

Background

Even though you will execute end-to-end tests that can help uncover problems with page flow, it is generally easier to verify page flow without involving your application's business logic, presentation layer, external resources, and so on. What we recommend you do is translate your Web application into a large state diagram where moving from page to page depends on two things:

  1. The action the user took
  2. The result of the action the user took

Some actions have a single outcome, such as clicking a link to browse a catalog. For the most part, that action cannot fail—when you click that link, you end up at the catalog page. Some actions have multiple outcomes, such as "OK" and "failed." At a registration page, for example, if submitting the registration form fails, then the shopper stays at the registration page. In your diagram, you can label those arrows using a format of action/result, so that "submit/OK" means "follow this arrow if submitting the form is successful." When we model page flow this way, it does not look very complex, and so it ought to be relatively easy to test—after all, they are just names of pages, names of actions, and names of results. They are all strings! How hard can that be?

Not hard at all. If you can focus your attention like this on just the page flow of your system, then certainly you can extract the page-to-page navigation code from your system into a single navigation "engine" that operates on navigation data. The Struts Web application framework3 works on this principle, and we discuss how much simpler it is to verify page flow on a Struts application in "Test Navigation Rules in a Struts Application." (We recommend James Turner and Kevin Bedell, Struts Kick Start (Sams Publishing, 2002) as well as Ted Husted et al., Struts in Action (Manning, 2002). The former is an excellent tutorial and the latter shows you what Struts can really do.)

This recipe shows a simple example of how to create a refactoring safety net, refactor navigation logic to a single class, and then verify the resulting data.

Recipe

We will start with what seems to be the simpler test: an end-to-end test that verifies the ability to move from one page to another. Returning to our Coffee Shop application, we first verify that we can move from the Welcome page to the Catalog page. We can use HtmlUnit to first load the Welcome page, and then push the Browse Catalog button and verify that the resulting Webpage is indeed the Coffee Catalog page. Of course, before we execute this test, we need to deploy the application to a live application server and start the server. You can see the test in question in Listing 1.

Listing 1. NavigationTest, a sample page flow test

package junit.cookbook.coffee.endtoend.test;
import java.net.URL;
import junit.framework.TestCase;
import com.gargoylesoftware.htmlunit.*;
import com.gargoylesoftware.htmlunit.html.*;
public class NavigationTest extends TestCase {
   private WebClient webClient;
   protected void setUp() throws Exception {
      webClient = new WebClient();
      webClient.setRedirectEnabled(true);
   }
   public void testNavigateToCatalog() throws Exception {
      Page page =
         webClient.getPage(
            new URL("http://localhost:8080/coffeeShop/"));
      assertTrue(
         "Welcome page not an HTML page",
         page instanceof HtmlPage);
      HtmlPage welcomePage = (HtmlPage) page;
      HtmlForm launchPointsForm =
          welcomePage.getFormByName("launchPoints");
      HtmlInput htmlInput =
         launchPointsForm.getInputByName("browseCatalog");
      assertTrue(
         "'browseCatalog' is not a submit button",
         htmlInput instanceof HtmlSubmitInput);
      HtmlSubmitInput browseCatalogSubmit =
         (HtmlSubmitInput) htmlInput;
      Page page2 = browseCatalogSubmit.click();
      assertTrue(
         "Catalog page not an HTML page",
         page2 instanceof HtmlPage);
      HtmlPage catalogPage = (HtmlPage) page2;
      assertEquals(
         "Coffee Shop - Catalog",
         catalogPage.getTitleText());
   }
}

There are a number of things about this test that are worthy of concern. We need to deploy the application and start the application server in order to execute it. Although this seems like a reasonable requirement to verify page flow, any number of unrelated problems can make it impossible to execute this test: EJB (Enterprise JavaBeans) deployment problems, servlet/URL mapping problems, and so on. That is not to say that these problems are not important enough to fix, but we generally prefer to verify them separately. These tests are meant to verify page flow and nothing else.

The test hard-codes information about the server to which the application is deployed (localhost) and the port on which it is listening (8080). Both of these pieces of information vary from environment to environment, so at a minimum, you ought to refactor the information to some external source, such as a configuration file. This makes the tests slightly more complex to configure and execute correctly.

The test depends on the correctness of the Webpages themselves, which is not guaranteed. If there is a problem with either the Welcome page or the Catalog page, then this test might fail, even though the problem is not navigation related. Certainly you will test the pages themselves in isolation using the techniques in Chapter 12, "Testing Web Components," so there is no need to duplicate that effort here.

This test hard-codes information about the structure of the Webpages—the form named launchPoints and the submit button named browseCatalog. If someone changes these names, then these tests will fail, making them somewhat brittle. A Web author ought to be able to change that button to a link, get the URL right, and the only tests to fail would be the ones for the page itself. This test does not allow that to happen.

Now please do not get us wrong: the foregoing is not an indictment of HtmlUnit. Far from it. HtmlUnit is an excellent package that does a very good job of automating end-to-end tests. With its focus on analyzing the result of a Web request—its comprehensive HTML page object model—HtmlUnit is an ideal choice for automating end-to-end tests for Web applications, so HtmlUnit is not the issue here. The issue is using a hammer to kill a fly, as it were: using end-to-end tests (no matter how you write them) to verify page flow invites the kinds of problems we have just described. Instead, we ought to write tests that focus on the navigation rules themselves.

This is the kind of test we want to write, assuming the existence of an object representing a "navigation engine:"

public void testNavigateToCatalog() {
   assertEquals(
      "Catalog Page",
      navigationEngine.getNextLocation(
         "Browse Catalog",
         "OK"));
}

This test simply says, "If I push the button marked Browse Catalog and everything goes OK, then I should be taken to the Catalog page." The test is expressed in a somewhat abstract fashion in terms of locations, actions, and results: a location is usually a Webpage, an action is usually either submitting a form or clicking a link, and a result is a description of the result of the action. If we want to talk in terms of a finite state machine, the locations are the machine's states and the action/result pairs are the machine's transitions. We can model navigating through our site entirely in terms of locations and actions.

A location corresponds to the URI of a Webpage or a Webpage template (Velocity template or JSP (JavaServer Pages)). An action corresponds to the URI of a form submit button or of a hypertext link. This means that we need some way to translate incoming request URIs into locations and actions, and vice versa. Once we give each URI the name of either a location or an action, we can ignore the details of which JSP displays the catalog page or which request parameter indicates "add a product to the shopcart." We can test that all separately.

First, Listing 2 shows our Coffee Shop Controller using a separate requesttoaction mapper.

Listing 2. CoffeeShopController using an Action Mapper

private void handleRequest(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
   String forwardUri = "index.html";
   String userName = "jbrains";
   try {
      String actionName = actionMapper.getActionName(request);
      log("Performing action: " + actionName);
      if ("Browse Catalog".equals(actionName)) {
         CoffeeCatalog catalog = model.getCatalog();
         CatalogView view = new CatalogView(request);
         view.setCatalog(catalog);
         forwardUri = view.getUri();
      }
      else if ("Add to Shopcart".equals(actionName)) {
         AddToShopcartCommand command =
            makeAddToShopcartCommand(request);
         executeCommand(command);
      }
      else {
         log("I don't understand action " + actionName);
      }
   }
   catch (Exception wrapped) {
      throw new ServletException(wrapped);
   }
   request.getRequestDispatcher(forwardUri).forward(
      request,
      response);
}
1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more