StrutsTestCase: Drilled-down Testing for Struts-based Java Apps

ou can verify that your Web-based Java application accepts the proper inputs from the HTTP request stream and displays the proper results to the HTTP response stream by performing black box testing with HttpUnit. However, since black box testing essentially simulates a Web browser, the testing code interacts with the application code via HTTP communications, which hides the implementation details.

Unit testing, in contrast, focuses on these little details in the application code. When isolating and fixing bugs, seeing the implementation details that unit testing provides can be very useful. You can use HttpUnit for unit testing, but only when testing standard servlets. It doesn’t support the popular Struts Model-View-Controller (MVC) framework. For unit testing on Struts applications, you need StrutsTestCase, a small testing library that builds on top of the JUnit testing framework. With StrutsTestCase, you can actually access the server environment and fiddle with settings and inputs at a much more granular level than with black box testing. You also can access the server environment after the response.

Mock Testing Versus In-container Testing
StrutsTestCase allows you to unit test Struts-based Web applications in two ways:

  • Mock testing?Mock testing uses mock objects, which simulate the server API. More generally, a mock object is an object that simulates another object, usually an object in the environment you are testing under or an object within a library you are using within your application code.
  • In-container testing?In-container testing uses the container API, possibly with an intermediate layer of code that adds some testing functionality. You get all of the container’s abilities.

The primary downside with in-container testing is that setup is more complicated, and the efficiency of the code-test-bug fix loop is dependent on the container you use. If the servlet container requires a restart to reload modified classes and restarting the server takes a long time, then your code-test-bug fix cycle will be slower under in-container testing than under mock testing.

Another difference between the testing methods is that mock objects require a simulated object for every API function in the server environment. This can be cumbersome and tedious for the developer of the mock object library, so developers sometimes omit API functions. As a result, sometimes the mock object library may not support all of the functionality of the real server environment. For example, StrutsTestCase omits JNDI functionality. If you have JNDI bindings in the Tomcat server.xml file, you will either have to use in-container testing (which StrutsTestCase supports), or you will have to integrate a mock object with JNDI support and server.xml parsing capability into the StrutsTestCase testing library, which is not hard, but it takes some time if you are not familiar with StrutsTestCase.

Generally, adding onto the testing library you are using is a drag. After all, the testing library is supposed to handle most or all of your testing needs. The omission of API functions from the mock object library probably will hamper your ability to test when your application eventually grows complex. For example, the lack of JNDI support will probably become an issue. The traditional way to access a database is through a JNDI binding, which the container provides and the server.xml file (or other container configuration file) specifies. As your application gets more complex, you will probably gravitate towards in-container testing.

Fortunately, StrutsTestCase makes it easy to switch between mock testing and in-container testing. You choose mock testing or in-container testing by subclassing MockStrutsTestCase or CactusStrutsTestCase, respectively. You do not have to make any other changes to your code. Thus, if you want to switch from mock testing to in-container testing, simply do a textual search and replace to change all occurrences of “extends MockStrutsTestCase” to “extends CactusStrutsTestCase”.

Set Up the Environment
Make sure you have the following packages installed prior to setting up your testing environment:

  • Ant 1.6.1. Ant is the build tool you will use to build the Web application. It is available at ant.apache.org.
  • Tomcat 4.1.29. Tomcat is the servlet container in which your Web application will run. It is available at jakarta.apache.org/tomcat.
  • Struts 1.1. Struts is the MVC framework on which you’ll build your Web application. It is available at jakarta.apache.org/struts.
  • Cactus 1.5. Cactus is the in-container testing framework on which StrutsTestCase is built. It is available from jakarta.apache.org/cactus. Once you get to the download page, get the file at the link labeled “1.5 zip for J2EE API 1.3”. The Cactus distribution includes JUnit 3.8.1, so you do not have to download and install JUnit 3.8.1 separately.
  • Xerces 2.6.1. Xerces is a SAX-compliant and JAXP-compliant XML parser that StrutsTestCase uses to parse struts-config.xml and tiles-defs.xml. The main site is at xml.apache.org/xerces2-j/index. You can download it at www.apache.org/dist/xml/xerces-j. Download the file called “Xerces-J-bin.2.6.1.tar.gz”.

To keep files and directories organized, you should unpack each of the above packages to ~/packages (creating the ~/packages directory, if necessary) so that each one is a subdirectory of ~/packages. The remaining instructions in this article assume that you unpack files to ~/packages. If you choose a different location, make the corresponding changes.

Get the StrutsTestCase library from sourceforge.net. The project home page is at strutstestcase.sourceforge.net, in case you want to access other StrutsTestCase resources. The latest version of StrutsTestCase as of the writing of this article is StrutsTestCase 2.1, which complies with the Java Servlet 2.3 API and supports the following Java tools:

  • Struts 1.1
  • Cactus 1.5
  • JUnit 3.8.1

Once you have the strutstest210-1.1_2.3.zip file, unzip it. Unzipping it to ~/packages will create a directory called ~/packages/strutstest. If you use a different directory, make sure to substitute any occurrence of ~/packages/strutstest in the remainder of this article with the actual location of strutstest.

Download the phonelist-strutstestcase.tgz file from the “Download the Code” section in the left margin. It contains the same phone list application from my previous HttpUnit articles, “Build a Java Web App Using HttpUnit and the Test-driven Methodology“, Parts I and II. (Read these articles for a step-by-step guide to the phone list application’s development.) Unpack the phonelist-strutstestcase.tgz file in the ~/projects directory (create the ~/projects directory, if necessary). This should create a directory called phonelist-strutstestcase.

You now need to tailor the build.xml file in phonelist-strutstestcase, so that the directories point to the proper locations. Open up build.xml in your text editor and replace the values of the tomcat.install.dir, struts.install.dir, cactus.install.dir, xerces.install.dir, and strutstestcase.install.dir properties with the correct directory locations of Tomcat, Struts, Cactus, Xerces, and StrutsTestCase, respectively.

Running Mock Tests
Change to the ~/projects/phonelist-strutstestcase directory and type: ant run-mock-tests. That will run all of the mock tests, which do not require a running Tomcat server. The mock approach’s speed advantage is pretty clear. Not only is it faster to test in a static sense, but more importantly, the development cycle over time is much faster because you do not need to perform the following sequence of steps each time you test a modified piece of code:

  1. Shut down the servlet container (Tomcat or other container).
  2. Delete the files in the servlet container’s deployment directory.
  3. Copy the new files to the servlet container’s deployment directory.
  4. Restart the servlet container.

All you have to do is let Ant compile the changed classes, and then you jump straight into executing the code. If you look at the build.xml file, you see that the run-mock-tests target invokes two tests: edit and save. The targets are called test-mock-edit and test-mock-save, respectively.

The “edit” test is fairly simple. The following is the bare class definition of the TestEditAction.java file located in the src/test/com/abcinc/phonelist/test/mock subdirectory of the ~/projects/phonelist-strutstestcase directory:

public class TestEditAction extends MockStrutsTestCase {  public TestEditAction(String testName) { super(testName); }  public static void main(String args[]) {    junit.textui.TestRunner.run(suite());  }  public static TestSuite suite() {    return new TestSuite(TestEditAction.class);  }  public void testEdit() {    setRequestPathInfo("/showList");    actionPerform();    setRequestPathInfo("/edit");    addRequestParameter("id", "1");    actionPerform();    EditForm editForm = (EditForm)getActionForm();    Integer id = editForm.getId();    assertEquals("id value", id, new Integer(1));    verifyForward("success");  }}

The file you downloaded contains import statements and comments at the top (for brevity, I omitted the comments). Note that the class extends MockStrutsTestCase. All StrutsTestCase tests inherit either from MockStrutsTestCase or CactusStrutsTestCase. You could easily turn it into an in-container test by changing MockStrutsTestCase to CactusStrutsTestCase.

Let’s examine the testEdit() method. (My previous HttpUnit articles explain the purposes of the constructor, main, and suite() methods.) The general pattern for a StrutsTestCase test is the following:

  1. setRequestPathInfo(): sets the action path that will be executed subsequently. The parameter you pass in is the context-relative URI.
  2. addRequestParameter(): adds one or more parameters to the request object. These are passed to the Struts action you call as if they came from the Web browser.
  3. setActionForm(): sets the ActionForm object with an initialized ActionForm containing values you define.
  4. actionPerform(): calls the Struts action specified in setRequestPathInfo().
  5. verifyNoActionErrors(): verifies that the Struts action did not flag any errors.
  6. getActionForm(): gets the ActionForm object and checks values in the ActionForm against known values.
  7. assertXxx(): checks any assertions that you wish.
  8. verifyForward(): verifies that the ActionForward is the right value.

You can repeat the cycle as many times as you wish within a method of a TestCase class. In the example above, you can see that the “/showList.do” action is called first. The “.do” extension is optional. If you do not specify it, StrutsTestCase will automatically append the “.do” extension. After you do that, you then call the “/edit.do” action, passing in a request parameter named “id” with a value of “1”.

Note that you can access the ActionForm object (in this case, a subclass of ActionForm named EditForm). This is something that would not be possible with black box testing. You can get access to not only ActionForm but also the ActionServlet controller, the HttpSession object, and other objects that are accessible only within the servlet container. As a result, you have more control over your testing and can test more detailed conditions with the unit testing capability StrutsTestCase offers.

Take a look at the TestSaveAction located in the same directory as TestEditAction. It has very similar functionality to TestEditAction, but has more meat to it.

As you can see, mock testing is fairly straightforward. It can also be limiting in some circumstances, as shown by the lack of support for JNDI and some other services provided by the servlet container. Mock objects are not intended to replicate the servlet container completely. The good news is that StrutsTestCase makes it easy to change a mock test to an in-container test.

Running In-container Tests
To run the tests using an in-container approach, you normally need to perform the following additional setup steps (which I have already done in the download file):

  1. Add four blocks of data to the web.xml file (see below).
  2. Make sure that a cactus.properties file exists in the classpath when you execute the Cactus test.
  3. Copy the Cactus and StrutsTestCase JAR files to the WEB-INF/lib directory of the packed WAR file containing your application.
  4. Copy the cactus.properties file to the WEB-INF/classes directory (or somewhere in the classpath).
  5. Copy the test classes (e.g., TestDeleteAction.java) to the WEB-INF/classes directory at the right place in the hierarchy.

The following two blocks of servlet configurations are required in web.xml:

        ServletRedirector          org.apache.cactus.server.ServletTestRedirector              ServletTestRunner          org.apache.cactus.server.runner.ServletTestRunner      

Additionally, you need to add two corresponding servlet mappings in web.xml:

        ServletRedirector    /ServletRedirector          ServletTestRunner    /ServletTestRunner  

The cactus.properties file should contain a property called cactus.contextURL at minimum. Normally, the line should say something like cactus.contextURL=http://localhost:8080/test, where localhost is the name of the host running Tomcat (many times this is the literal string “localhost”) and /test is the context path of the Web application. In this case, the context path of the Web application is /phonelist-strutstestcase. So the full value of cactus.contextURL should be http://localhost:8080/phonelist-strutstestcase. I put the cactus.properties file in the ~/projects/phonelist-strutstestcase/src/test/com/abcinc/phonelist/test/container directory, but you can put it anywhere in the classpath.

To learn how the JAR files, cactus.properties file, and test classes are copied and placed in the packed WAR file, consult the build.xml file?specifically, the copy-unprocessed-web-files, copy-unprocessed-test-files, and package-war targets.

Time to Run the Tests
You are now ready to run the tests. First, start up Tomcat. Then, deploy the tests by typing ant deploy. Now type ant run-container-tests. You should see something similar to the following:

Buildfile: build.xmlinit:create-unpacked-layout-dirs:compile-classes:create-test-dirs:compile-tests:post-compile-tests-init:test-container-delete:     [java] .     [java] Time: 0.985     [java] OK (1 test)run-container-tests:BUILD SUCCESSFULTotal time: 3 seconds

You can invoke an undeploy target by typing ant undeploy. The undeploy target removes the phonelist-strutstestcase application and the server-side Cactus tests (which are embedded in the phonelist-strutstestcase application) from the Tomcat deployment directory. You should then shut down Tomcat and restart it. Type ant deploy again to deploy the application again. This sequence of steps updates Tomcat with changes to either your Web application or the Cactus tests you write.

Let’s examine what the TestDeleteAction class does. Note that the class inherits from CactusStrutsTestCase, which allows it to perform in-container testing. The first important piece of code to examine is the testDelete method, because the rest of the code in the TestDeleteAction class (composed of the import statements and the other methods) is a boilerplate that you have already seen. Here is the testDelete method stripped of comments:

  public void testDelete() {    setRequestPathInfo("/showList");    actionPerform();    List contactBeanList = null;    int oldSize = -1;    contactBeanList = ContactDatabase.getList();    oldSize = contactBeanList.size();    setRequestPathInfo("/delete");    Integer[] deletedIdArray = new Integer[] {new Integer(0), new Integer(2),
new Integer(4)}; String[] deletedIdStringArray = new String[deletedIdArray.length]; for(int i = 0; i < deletedIdArray.length; i++) { deletedIdStringArray[i] = deletedIdArray[i].toString(); } addRequestParameter("id", deletedIdStringArray); actionPerform(); verifyForward("success"); contactBeanList = ContactDatabase.getList(); int newSize = contactBeanList.size(); assertEquals("oldSize - newSize", oldSize - newSize, deletedIdArray.length); boolean deletedElementStillPresent = false; for(int i = 0; i < contactBeanList.size(); i++) { ContactBean contactBean = (ContactBean)contactBeanList.get(i); for(int j = 0; j < deletedIdArray.length; j++) { Integer id = deletedIdArray[j]; if (contactBean.getId().equals(id)) { deletedElementStillPresent = true; break; } } if (deletedElementStillPresent) { break; } } assertFalse("deletedElementStillPresent", deletedElementStillPresent); try { InitialContext iniCtx = new InitialContext(); String stringFromJndi = (String)iniCtx.lookup("java:comp/env/aJndiBinding"); assertEquals("aJndiBinding", stringFromJndi, "Jonas Salk"); } catch (NamingException e) { throw new RuntimeException(e); } }

The code above has several interesting aspects. First, I call ContactDatabase.getList() on the seventh line. Remember that ContactDatabase is a server-side construct, so normally you would not be able to access it directly with black box testing. In addition to retrieving data from the container's environment, I can also modify data in the container's environment and call classes directly to exercise their full functionality and isolate bugs quickly.

Note the JNDI access also. In this case, the retrieval of the JNDI binding is a contrived example because it has nothing to do with deleting contacts. However, it illustrates how an in-container test has access to all of the data in the container environment, including JNDI bindings. Accessing the JNDI binding for a String environment variable is marginally useful. Accessing a JNDI binding for a database connection, a database pool, or other hardware or software resources is essential because your application may require that access. It is difficult, if not impossible, to test an application when all of its required resources are not available.

Keep Mindful of Your Tests
Your foray into unit testing with StrutsTestCase should arm you with the knowledge necessary to begin outfitting your Web applications, whether Struts-based or not, with tests that improve robustness and accelerate the software development process. As you begin to use the testing frameworks described in my previous HttpUnit articles and this article, keep in mind that tests are only as good as the thought you put into them. You should exercise as much care with tests as with the application code.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: