Previous Section
Table of Contents
Next Section


Unit Testing Struts Applications

If a developer follows the best practices of Web application design-specifically, use of the Model-View-Controller (MVC) design pattern supported by Struts-most of the critical unit tests will be written for testing the model, and the Struts-specific tests will be only a small portion of the application's unit test suite. A well-designed Struts application promotes the definition of actions that delegate processing to business components and services that reside in the model. The Action classes are responsible only for transferring data from the View layer to the Model layer and vice versa.

The bulk of the unit tests required for a Struts application should be for the business components and services. JUnit (http://www.junit.org), developed by Erich Gamma and Kent Beck, is an excellent framework for unit testing these types of Java objects. This framework provides the software infrastructure for developing tests. It has become the de facto standard for Java unit testing. JUnit provides a base test class that is extended by the developer to create a specific unit test. The developer then writes test methods in this new test class. The test methods exercise the class to be tested and verify that the class being tested behaves as expected.

JUnit provides several features that make writing and running these tests simple. First, JUnit treats any method in the test class that begins with the word 'test' as a method to run. Therefore, the developer uses that convention to name the test methods (e.g., testSearchByName, testSuccess). Within the test, JUnit provides methods for performing the verifications. These methods are referred to as assertions. An assertion simply expresses a Boolean (true/false) relationship. For example, you may assert that a value returned from a method is not null, or that the returned value has a specific value. If an assertion fails, that particular test method fails. Finally, JUnit provides several test runners that run the tests and report the test results. The following section describes how JUnit can be applied to the sample Mini HR application.

Testing the Model

The EmployeeSearchService class, first introduced in Chapter 2, is the primary business component of the Mini HR application. It provides methods to search for employees by name or social security number. Following is a JUnit unit test for this service. Note that the unit test can be run independently of the Web application and servlet container. Its only dependency is to the class being tested.

package com.jamesholmes.minihr;

import java.util.ArrayList;
import junit.framework.TestCase;

public class EmployeeSearchServiceTest extends TestCase {
  private EmployeeSearchService service;

  public EmployeeSearchServiceTest(String arg0) {
    super(arg0);
  }

  public void setUp() {
    service = new EmployeeSearchService();
  }
  public void tearDown() {
    super.tearDown(); 
  }

  public void testSearchByName() {
    String name ="Jim";
    ArrayList l = service.searchByName(name);
    assertEquals("Number of results", 2, l.size());
  }

  public void testSearchBySSN() {
    String ssn ="333-33-3333";
    ArrayList l = service.searchBySsNum(ssn);
    assertEquals("Number of results", 1, l.size());
    Employee e = (Employee) l.get(0);
    assertEquals("SSN", ssn, e.getSsNum());
  }

  public void testSearchByUnknownSSN() {
    String ssn ="999099-49493939";
    ArrayList l = service.searchBySsNum(ssn);
    assertEquals("Number of results", 0, l.size());
  }
}

First, notice that the test class is in the same package as the EmployeeSearchService class and uses the Class under testTest naming convention. If the test class were in a different package, only public methods could be tested. The next thing to notice is the setUp( ) method, which is used to prepare each test. Here, this method creates the EmployeeSearchService object and stores a reference to it in an instance variable. As a corollary, a tearDown( ) method can be implemented to perform any test cleanup. That would be the place, for example, to close a database connection or file resource used by the test.

A JUnit test runner actually runs the test. JUnit includes three test runner user interfaces-one based on Java AWT, one based on Java Swing, and a text-based runner that can be run from the command line. In addition, many Java IDEs provide some level of integration with JUnit. The test runner first creates a new instance of the test class. Then, for each test method (i.e., each method that begins with 'test'), the runner calls setUp( ), then the test method itself, and then tearDown( ). This approach allows for the creation of independent, repeatable tests. Each test method should be capable of running separately, with the setUp( ) and tearDown( ) methods performing the initialization and cleanup.

The actual verification part of the test is encapsulated in the individual test methods. Generally speaking, there should be at least one test method for each method of the external interface of a class under test. The test method should create the input values, call the appropriate method, and then verify the results. This verification may include examining any returned values as well as checking any expected state changes. Each verification is implemented using one or more of the JUnit assert( ) methods. For example, if a search by social security number is performed using a value for which there is no match, then an assertion can be made that the returned collection should be empty. This type of negative test is illustrated in the EmployeeSearchServiceTest class in the testSearchByUnknownSSN( ) method.

To run this test, ensure that the junit.jar file and the application classes are on your classpath. Then, simply execute the following command:

java junit.swingui.TestRunner

This displays the JUnit graphical user interface (GUI), shown in Figure 20-1, which allows you to select the test to run. You can then run the test and view the results. If you get a green bar, then all tests succeeded. If the test fails, you can change the code to make it work-then rerun the test. Usually, you do not need to restart the JUnit Swing user interface because it incorporates a mechanism for reloading classes.

Click To expand
Figure 20-1: A successful test run using the JUnit Swing test runner

Now that you've seen how to write a unit test, you need to integrate unit testing into the build process. Apache Ant (http://ant.apache.org) is a Java-based build tool that provides support for integrating testing into an application's build cycle. An Ant build is driven by an XML-based build file. The build file includes specific targets that correspond to steps in the build process, such as compiling classes, creating the distribution (such as a .jar file), and deploying the application. Testing can be added to the build cycle by including testing targets in the application's Ant build script. This chapter will not go into complete details of integrating JUnit with Ant; however, it is fairly straightforward and the Ant support for unit testing is well documented in Ant's documentation set. Following is the Ant build script, build.xml, that will be used and expanded throughout this chapter. The test target is the last target in this file.

<project name="MiniHR20" default="dist" basedir=".">
  <property environment="env"/>
  <property name="build.compiler" value="javac1.4"/>
  <!-- set global properties for this build -->
  <property name="src" location="src"/>
  <property name="webinf.dir" location="web/WEB-INF"/>
  <property name="build.dir" location="build"/>
  <property name="dist.dir"  location="dist"/>
  <property name="server.dir" location="${env.CATALINA_HOME}"/>
  <property name="servlet.jar" 
  <property name="test.dir" location="test"/>
  <property name="deploy.dir" location="${server.dir}/webapps"/>

  <target name="clean">
    <delete dir="${build.dir}"/>
    <delete dir="${dist.dir}"/>
  </target>

  <target name="init">
    <mkdir dir="${build.dir}"/>
    <mkdir dir="${dist.dir}"/>
  </target>

  <target name="compile" depends="init"
        description="compile the source ">
    <javac srcdir="${src}:${test.dir}/src"
           destdir="${build.dir}" debug="on">
      <classpath>
        <pathelement path="${classpath}"/>
        <pathelement location="${servlet.jar}"/>
        <fileset dir="${webinf.dir}/lib">
          <include name="**/*.jar"/>
         </fileset>
      </classpath>
    </javac>

    <copy todir="${build.dir}">
      <fileset dir="${src}">
        <include name="**/*.properties"/>
      </fileset>
    </copy>
  </target>

  <target name="dist" depends="compile">
    <war destfile="${dist.dir}/${ant.project.name}.war"
         webxml="${webinf.dir}/web.xml">
      <webinf dir="${webinf.dir}">
        <include name="struts-config.xml"/>
        <include name="validation.xml"/>
        <include name="validator-rules.xml"/>
      </webinf>
      <classes dir="${build.dir}"/>
      <fileset dir="web" excludes="WEB-INF/**.*"/>
    </war>
  </target>

  <target name="deploy" depends="dist">
    <unjar src="${dist.dir}/${ant.project.name}.war"
           dest="${deploy.dir}/${ant.project.name}"/>
  </target>
  
  <target name="test" depends="compile">
    <mkdir dir="${test.dir}/results"/>
    <junit printsummary="yes" fork="no" haltonfailure="no" 
           errorProperty="test.failed" failureProperty="test.failed">
      <formatter type="xml" />
      <classpath>
        <pathelement path="${build.dir}"/>
      </classpath>
      <batchtest todir="${test.dir}/results">
        <fileset dir="${build.dir}">
          <include name="**/*Test.class" />
        </fileset>
      </batchtest>
    </junit>
    <junitreport todir="${test.dir}/reports">
      <fileset dir="${test.dir}/results">
        <include name="TEST-*.xml"/>
      </fileset>
      <report format="frames" todir="${test.dir}/reports"/>
    </junitreport>
    <fail message="JUnit tests failed. Check reports." if="test.failed"/>
  </target>
</project>

The test target in the preceding build script integrates testing into the build. The junit task is used to run tests and generate the results. The junitreport task is used to accumulate and generate an HTML report from those results. These tests are run by simply typing ant test at a command prompt. The reports are generated into the test/reports directory. The generated report includes information about which tests passed, which ones failed, and the specific assertions that failed. If an exception was thrown, the stack trace is included with the report. In addition, some basic timing information is also recorded. As mentioned previously, testing the model should be the top priority when determining what aspects of the system to test first.

Testing Controller Behavior

Assuming that unit tests have been completed for the Model portion of the application, the next step is to test Controller behavior. Here the focus is on verifying the behavior of Struts actions. You want to check that an action performs as expected. This includes ensuring that the action does such things as the following:

  • Receives the data it needs from the request or session

  • Handles cases properly where required input is missing or invalid

  • Calls the model passing the correct data

  • Marshals and transforms as needed the data from the model

  • Stores the data in the appropriate context or scope

  • Returns the correct action response

These types of tests can be performed in a running container (i.e., a servlet engine such as Tomcat) or outside of the container in a normal client JVM. This is an important distinction. Test execution outside the container allows you to isolate the class under test from outside dependencies. When the test fails, you know that the failure is in the class under test, not a problem with the container.

In-container tests are a form of integrated unit tests. Integrated unit tests can be valuable for verifying that the class being tested will work in an actual deployed environment. Practically speaking, in-container tests are more complex to set up and run and generally take more CPU time than tests run independently of a container. On the other hand, outside-the-container tests often require creation of mock objects to stand in for container-provided software entities such as the servlet request and response objects, the servlet context, and the HTTP session.

Using StrutsTestCase

StrutsTestCase (http://strutstestcase.sourceforge.net) is an open-source JUnit extension that provides a framework of Java classes specifically for testing Struts actions. It provides support for unit testing Struts actions outside of a servlet container using mock objects. These objects are provided as stand-ins for the servlet request and response, and context components such as the servlet context and the HTTP session. Test cases using the mock objects use information from your application's configuration files-specifically the web.xml file and the struts-config.xml file. In addition, StrutsTestCase also supports in-container testing through integration with the Cactus framework. In-container testing will be covered in 'Using Cactus for Integration Unit Testing' later in this chapter. Since StrutsTestCase extends JUnit, the tests can be run using the JUnit test-running tools. Consider a unit test for the SearchAction class of the sample application. This action performs the following steps:

  1. Gets the entered name from the form.

  2. Determines whether the search is by name (i.e., a name was entered) or by social security number.

  3. Performs the search using EmployeeSearchService.

  4. Stores the results back in the form.

  5. Forwards the request back to the input page.

Here's a unit test that verifies this behavior:

package com.jamesholmes.minihr;

import servletunit.struts.MockStrutsTestCase;

public class SearchActionTest extends MockStrutsTestCase {
  public SearchActionTest(String testName) {
    super(testName);
  }

  public void setUp() throws Exception {
    super.setUp();
    setRequestPathInfo("/search");
  }

  public void testSearchByName() throws Exception {
    addRequestParameter("name","Jim");
    actionPerform();
    assertNotNull("Results are null",
      ((SearchForm) getActionForm()).getResults());
    verifyInputForward();
  }
}

First, notice that the class inherits from servletunit.struts.MockStrutsTestCase, which in turn inherits from the JUnit TestCase (junit.framework.TestCase). This relationship allows access to all the assertion methods provided by JUnit. Next, in the setUp( ) method, the parent's setUp() method is called and the request path is set. Both of these steps are critical. If you override setUp( ), you must call super.setUp( ). Setting the request path ensures that the desired action is called. Notice that the .do extension mapping is not included in the request path. StrutsTestCase uses the web.xml file to determine this. Note also that there is no reference in the test to the SearchAction class itself. StrutsTestCase actually relies on the action mappings in the struts-config.xml file to determine which action to call. So, in reality, you are testing not just the SearchAction class, but the entire action configuration.

The test method itself is relatively simple. First, a request parameter is added that represents the data that would be submitted from the HTML form on the search.jsp page. Next, actionPerform( ) actually executes the action. After that, the resultant behavior is verified. Specifically, the results property of the SearchForm object is checked for null. In addition, the forward back to the input page (search.jsp) is verified using the verifyInputForward( ) method.

To run this test, some changes need to be made to the test target of the Ant script. Since StrutsTestCase relies on the XML configuration files of the application, the parent directory of the application's WEB-INF directory must be on the classpath. The following three entries should be added to the classpath element of the junit task:

  <fileset dir="${webinf.dir}/lib"/>
  <pathelement location="${servlet.jar}"/>
  <pathelement path="web"/>

The first entry adds all .jar files in the WEB-INF/lib folder. The second element adds the servlet.jar file. Finally, the third element specifies the directory that contains the WEB-INF directory.

The use of StrutsTestCase for mock testing is extremely powerful. The underlying mock implementation allows your test to focus on the specific unit being tested. In addition, the tests can be run without requiring a running container with the application deployed. If you are in an environment where the container is running on a different server, likely even a different operating system, you can still unit test your actions on your local development computer. That being said, these unit tests do not take into account deployment issues, container dependencies, and other run-time behaviors that can only be tested with a running application. Fortunately, there is a tool that can help with this testing also.

Using Cactus for Integration Unit Testing

Some would consider integration unit testing a bit of an oxymoron. Typically, unit tests should be written such that they are isolated from outside dependencies. Isolating the unit test makes it much easier to identify the cause of errors when a test fails. However, there certainly is a place for integrated testing. A counter-argument can be made that an integrated unit test gives you a more realistic test. For some types of objects, say for example, Enterprise Java Beans, certain aspects of the objects can only be tested in the container. Container-provided services such as transactions and persistence are not easily mocked up. Another case where integration tests are invaluable is with regression testing. Deployments to different application servers and different operating systems can be verified using integrated unit tests.

Cactus was developed to provide these types of unit tests. It was originally developed to test Enterprise JavaBeans but is equally up to the task of testing servlets, servlet filters, and JSPs. In fact, Cactus can be used to test any type of behavior that relies on a J2EE/servlet container. As will be shown later in this section, Struts actions can be tested through interaction with Struts' ActionServlet. However, these tests do come at a cost of increased complexity and slower performance. Cactus tests are fairly easy to write, but configuration and deployment can be complex. If you are running your unit tests frequently as part of your build process, you may find that your build takes far too long. The reason Cactus tests take longer is that Cactus starts your application server every time it runs its suite of tests. A good option is to only run Cactus periodically, such as on nightly automated builds. Here are some guidelines to help you decide what type of testing to use and when:

  • Concentrate on unit tests for the model, first. That is where most of the business logic is and that should be the focus.

  • Use the StrutsTestCase with mock objects to test your Action classes. Tests using mock objects can be run much faster, and do not require a servlet container.

  • Use Cactus testing for those classes that rely on container-provided services. For example, if you are using JNDI, or testing behavior based on container-managed security, you should use Cactus.

In addition, the Cactus site itself includes an even-handed comparison of mock object testing to Cactus in-container tests.

To get started with Cactus, you need to download the distribution. As of this writing, Cactus 1.5 was the latest release build. Cactus can run tests via integration with your IDE; however, the preferred approach is via Ant. First, you need to define the task definitions for the Ant tasks that Cactus provides:

  <path id="cactus.classpath">
    <fileset dir="cactus/lib">
      <include name="*.jar"/>
    </fileset>
  </path>
  <taskdef resource="cactus.tasks"
    classpathref="cactus.classpath"/>

This defines a path containing the Cactus .jar files and creates the Cactus task definitions.

Next, add the Cactus classpath to the classpath of the compile target. Use cactus .classpath as the reference ID for the path element. As stated, Cactus tests will make the build slower; therefore, create a separate test.cactus target. In addition, provide a test wrapper target for running both test sets. The test.cactus target performs the following:

  1. Instruments the application's .war file for testing by creating a new .war file. This process is referred to as cactifying the .war file.

  2. Runs the unit tests. This is done using the cactus task. The containerset element indicates the container to run the tests against. This task extends the junit task. It uses the same subelements for specifying the results format and the set of tests to run.

  3. Creates an HTML report of the results. This is done using the junitreport task that was shown previously.

Now the test case needs to be written. Cactus is an extension of JUnit. Cactus provides base classes that extend junit.framework.TestCase. If you have written a JUnit test, Cactus tests will be familiar. However, since Cactus runs on both the client and server, there are some additional APIs that are of use. Before delving into test code, a review of how Cactus works is in order.

One of the most important aspects of Cactus to understand is that a Cactus test runs in two JVMs. Specifically, there is a copy of the test on the client JVM, and a copy on the server JVM (that is, the JVM running the J2EE/servlet container). From Figure 20-2 you can see that Cactus provides hooks on both the server and client side. The server-side hooks are the standard JUnit fixture methods-setUp( ) and tearDown( ). These methods are run, on the server, before and after execution of each testYourMethod( ) method. The begin( ) and end( ) methods are unique to Cactus. These methods are executed on the client side-before the request is sent, and after the response is received. Since there are two copies of the test, instance variables set in the begin( ) method are not set on the server side. Likewise, variables set in setUp( ) and tearDown( ) are not available in the client-side begin( ) and end( ) methods.

Click To expand
Figure 20-2: Cactus architecture

These optional methods can be used to simulate the request and interrogate the response. Think of these methods as the browser side of the test. To make things concrete, the SearchActionTest class, implemented before as a MockStrutsTestCase, is reimplemented in the following code as a Cactus ServletTestCase. As mentioned, Cactus does not provide a direct way of testing a Struts action, as StrutsTestCase does. Struts actions are tested indirectly by interacting with the ActionServlet. Here is the Cactus unit test for testing the SearchAction class:

package com.jamesholmes.minihr;

import org.apache.cactus.ServletTestCase;
import org.apache.cactus.WebRequest;
import org.apache.struts.action.ActionServlet;

public class SearchActionCactusTest extends ServletTestCase {
  private ActionServlet servlet;

  public SearchActionCactusTest(String theName) {
    super(theName);
  }

  public void setUp() throws Exception {
    servlet = new ActionServlet();
    servlet.init(config);
  }

  public void beginSuccess(WebRequest request) {
    request.setURL(null, // server name (e.g. 'localhost:8080')
    null, // context (e.g. '/MiniHR20')
    "/search.do", // servlet path 
    null,         // extra path info
    "name=Jim");    // query string
  }

  public void testSuccess() throws Exception {
    servlet.doGet(request, response);
    SearchForm form = (SearchForm) request.getAttribute("searchForm");
    assertNotNull(form.getResults());
  }

  public void tearDown() throws Exception {
    servlet.destroy();
  }
}

There are key differences between this test and the previous mock test. First, notice that the setUp() method creates an instance of the Struts ActionServlet itself. Then, in the beginSuccess( ) method, the URL of the request object is set to the page to access and the query string. The SearchAction class is executed indirectly by calling the doGet( ) method of the ActionServlet. Essentially, the test is emulating the behavior of the container. Also notice that the request path must include the .do extension-Cactus knows nothing about Struts or the ActionServlet's mapping. Finally, in the testSuccess( ) method, the request is queried for the form. Note that the attribute name 'searchForm' had to be hard-coded into the test. Using StrutsTestCase, this information was pulled from the struts-config.xml file. Simply changing the name of the form in the struts-config.xml file will break this Cactus test.

As you might suspect, these tests can become rather brittle. The tests are dependent on configuration settings that are outside the scope of the test. It would be preferable if there were a base class that could get configuration information from Struts. Fortunately, StrutsTestCase provides just that.

Using StrutsTestCase with Cactus

StrutsTestCase actually provides two base classes that can be used to create Action tests. MockStrutsTestCase, which extends the JUnit TestCase, has already been demonstrated. Tests that extend this class can be run outside the container. For in-container tests, StrutsTestCase includes the CactusStrutsTestCase base class. This class extends the Cactus ServletTestCase. The UML diagram in Figure 20-3 shows how these objects relate.

Click To expand
Figure 20-3: JUnit/StrutsTestCase/Cactus relationships

A MockStrutsTestCase can easily be turned into a container-driven Cactus test case by simply changing the class it extends to the CactusStrutsTestCase. You may find that you will want to have both forms of these tests. In fact, you could probably refactor the common test behavior (that is, the setup, assertions, and verifications) into a common class that is used by both the mock test and the in-container test. The previous StrutsTestCase can be changed to run under Cactus by simply changing it to extend from CactusStrutsTestCase-the rest is the same.

package com.jamesholmes.minihr;

import servletunit.struts.CactusStrutsTestCase;

public class SearchActionStrutsTest extends CactusStrutsTestCase {
  public SearchActionStrutsTest(String testName) {
    super(testName);
  }

  public void setUp() throws Exception {
    super.setUp();
    setRequestPathInfo("/search");
  }

  public void testSearchByName() throws Exception {
    addRequestParameter("name","Jim");
    actionPerform();
    assertNotNull("Results are null",
      ((SearchForm) getActionForm()).getResults());
    verifyInputForward();
  }
}

As you can see, the code is much simpler and easier to maintain than the same test written using the Cactus framework alone. Moreover, because it extends the Cactus classes, the test can easily be run as an in-container test that more closely tests actual deployment.

Now that you know how to test actions both in and out of the container, you should consider the additional verifications that StrutsTestCase provides. Both MockStrutsTestCase and CactusStrutsTestCase extend TestCase and therefore offer all the standard assertion methods that are available with the JUnit API. In addition, several Struts-specific assertions are available in both of the StrutsTestCase base classes. All of these methods begin with verify:

  • verifyActionErrors( )  Verifies that a specific set of error messages, identified by key, were sent

  • verifyNoActionErrors( )  Verifies that no errors were sent

  • verifyActionMessages( )  Verifies that a specific set of action messages, identified by key, were sent

  • verifyNoActionMessages( )  Verifies that no action messages were sent

  • verifyForward( )  Verifies that the controller forwarded to a specified logical forward name, global or local, as specified in the Struts configuration file

  • verifyForwardPath( )  Verifies that the controller forwarded to a specified absolute path

  • verifyInputForward( )  Verifies that the controller forwarded to the path identified by the action mappings input attribute

  • verifyTilesForward( )  Verifies that the controller forwarded to a specified logical forward name from the Struts configuration and a Tiles definition name from the Tiles configuration

  • verifyInputTilesForward( )  Verifies that the controller forwarded to the defined input of a specified Tiles definition

Before leaving the subject of in-container tests, it is worth mentioning that Cactus also provides a base class for testing servlet filters. This could be used, for example, to test the authorization filter used in Chapter 19.

Testing the View

Verifying that an application's presentation is correct is a critical step in software development. You are never quite sure if the view is appropriate until you put it before the customer. All too frequently, countless hours are spent building a complex system without regard to the presentation. Testing these views typically falls under the heading of functional testing and acceptance testing. However, there is important behavior that a view provides that can be unit tested. Most Struts applications leverage the rich Struts tag library in JSP pages to provide dynamic HTML rendering. The Logic Tag Library tags, for example, can be used to selectively render HTML markup in a JSP page. Such pages can be unit tested by supplying the appropriate inputs to the page, accessing the page, and then verifying the response. The difficulty comes in two areas. First, the inputs may come from the container-managed objects such as the HTTP request, HttpSession or ServletContext. Second, the response is generally in HTML- verifying that the correct HTML was rendered involves complex parsing.

Cactus helps address the first problem, as was shown earlier in the SearchAction CactusTest. These tests can be run in the container and the tests have access to the container-managed context objects. For parsing HTML, HttpUnit can be used. HttpUnit (http://httpunit.sourceforge.net) is a Java-based open-source framework for functional testing of Web sites. It provides Java APIs for accessing all aspects of the HTTP interaction. HttpUnit refers to this as the Web conversation.

One aspect of Cactus that has not been demonstrated is use of the WebResponse object provided by the endTest( ) method. Cactus supports two versions of this method. The first method provides access to a simple object, org.apache.cactus .WebResponse, which represents the HTTP response. With this object you can check the HTTP response status code and headers, check cookies, and assert actions based on the content. The content of the response is returned as a simple string. For a complex Web site, the returned content could be quite large. The other accepted version of the endTest( ) method receives an HttpUnit WebResponse object, com.meterware .httpunit.WebResponse. Contrary to the Cactus WebResponse, this object provides a rich API for accessing the HTML content of the returned response. Through this integration with HttpUnit, Cactus tests can navigate through the response in an object-oriented fashion, verifying that the response contains specific HTML details such as HTML tables, links, and forms.

Consider a unit test for the index.jsp page of the Mini HR application. The dynamic aspects of this page are conditioned on the presence of a User object in the session. An additional link is rendered if the user is an administrator, enabling the administrator to add an employee. A unit test for this JSP allows for these assertions. Here is a Cactus unit test that includes a test method that performs these assertions. This test allows the JSP to be tested in isolation.

package com.jamesholmes.minihr;

import org.apache.cactus.JspTestCase;
import com.jamesholmes.minihr.security.User;
import com.meterware.httpunit.WebLink;
import com.meterware.httpunit.WebResponse;

public class IndexJspTest extends JspTestCase {
  public IndexJspTest(String theName) {
    super(theName);
  }

  public void testAdministratorAccess() throws Exception {
    User user = new User("bsiggelkow","Bill", "Siggelkow", "thatsme",
                         new String[] {"administrator"});
    session.setAttribute("user", user);
    pageContext.forward("/index.jsp");
  }

  public void endAdministratorAccess(WebResponse response)
              throws Exception {
    // verify that the login form is not displayed
    assertNull("Login form should not have rendered",
    response.getFormWithName("loginForm"));
    //verify that the proper links are present
    WebLink[] links = response.getLinks();
    assertTrue("First link is admin/add.jsp",
      links[0].getURLString().startsWith("/test/admin/add.jsp"));
    assertTrue("Second link is search.jsp",
      links[1].getURLString().startsWith("/test/search.jsp"));
  }
}

The first thing you should notice is that the test imports the HttpUnit classes. HttpUnit is bundled with Cactus. (You may also want to get the latest HttpUnit distribution and documentation at http://httpunit.sourceforge.net.) Note also that the class extends JspTestCase instead of ServletTestCase. JspTestCase, which inherits from ServletTestCase, allows a JSP to be tested in isolation. The JSP page context can be accessed by the test class as needed. The test method was named to indicate that it is testing the display of the index page for a logged-in user with administrative privileges. Other conditions should also be modeled as test methods as appropriate. The first thing that the testAdministratorAccess( ) method does is instantiate a User object that is assigned the administrator role. This object is placed in the HttpSession under the name by which it will be accessed on the page. The page is then rendered by forwarding to the index.jsp page. The implicit PageContext object performs the job of forwarding the request.

The actual test assertions and verifications are now performed in the endAdministratorAccess( ) method. Remember that this method is executed in the client-side JVM after the request is processed. If a User object is in the session, the login form should not be displayed. The first assertion verifies this by asserting that the response does not contain a form named loginForm. Next, the set of links contained in the response is retrieved. Assertions are made that the first link is to the Add an Employee page (/admin/add.jsp), and that the second link is to the Search page (search.jsp). Initially, these assertions were written using the assertEquals( ) method as follows:

assertEquals("Check first link", "/test/admin/add.jsp",
  links[0].getURLString());

At first glance, this appears correct; however, if you run the test using this assertion, it most likely will fail, because the servlet container may append a session identifier string to the link. In this case, the actual value of the URL was the following:

/test/admin/add.jsp;jsessionid=0E5EFB6F64C01749EE94E3A57BDEBD21

Therefore, the assertion was changed to verify that the URL starts with the expected result.

While the tests just demonstrated certainly are useful, they are getting more into the realm of functional testing than unit testing. HttpUnit can be used for broader functional testing-not just unit testing JSPs. HttpUnit provides full support for the entire Web conversation. The API provides methods for constructing requests, following links, and populating and submitting forms. Tests can be written using this API that mimic expected usage scenarios of the Web application. However, the amount of code to support such a test can be extensive. Also, this code can be extremely sensitive to simple changes in presentation. In the following section, a better approach for this type of functional testing is presented.



Previous Section
Table of Contents
Next Section