StrutsTestCase: The tool for Struts unit testing

by Keld H. Hansen

Introduction

The Struts framework from the Jakarta project is one of the most successful open source projects around today. I'd say it's the de facto standard for implementing MVC (Model-View-Controller) servlet projects, and it's an obvious choice for smaller servlet applications. But, even in larger, enterprise EJB applications it's well suited as the servlet backbone for the application.

Part of every IT-project includes testing, and Struts projects are no different. If we look at testing "from the bottom up", we start by unit testing the classes and methods we're developing. Extreme programmers actually write their unit test cases before the code they're supposed to test! As the project progresses, we step up through the application architecture to module, integration and system testing. You may use different words for your test phases, but the idea is to start at the very detailed level and finish by letting the end users test the functionality of the system, in order to see that what they get is what was promised to be delivered!

This article is about unit testing a part of your Struts application, specifically how you test the Action class. Let's see where the Action class fits into the Struts architecture:

Figure 1: The Struts architecture

The Action class is the glue between the Struts ActionServlet and your business logic (the "Model"). Before testing your Action classes you'll normally have finished unit testing your business logic, and since these classes are normal Java classes which should have no connections to the servlet environment, you might have been using JUnit for the task. JUnit is also from the open source area, and it's just as popular as Struts. If you need to brush up your knowledge on JUnit I can recommend this tutorial on JUnit.      

If you know JUnit you'll be happy to also learn that an extension exists which makes it possible to test the Action classes. This may come as a surprise, since testing the Action class is quite another thing than testing a class from your business logic. When you test a method from the business logic you write a small program that calls the method, and afterwards you check the outcome of this call. Testing the Action class is the same as testing the execute method (in pre Struts 1.1 it was called perform), but how would you call this method? The signature of the method is this:

public ActionForward execute(ActionMapping mapping,
                             ActionForm form,
                             HttpServletRequest request,
                             HttpServletResponse response) 

 In order to make the call you'd have to supply four troublesome parameters, giving access to the Struts set-up and the servlet environment. No easy task. To our rescue comes StrutsTestCase.

Introducing StrutsTestCase

StrutsTestCase is an open source project from SourceForge, which extends the JUnit framework to allow testing the Action class. It even offers two approaches: one that uses the servlet container employing the Cactus framework from Jakarta, and one that simulates the container. This article will explain how to use the simulated set-up, or the "mock object approach". 

The idea behind the StrutsTestCase (STC) framework is that it's not you that calls the execute method, but STC. So it's up to STC to supply the four parameters to the execute method, and when you use the mock object approach, these parameters will be simulated. The request and response objects don't come from a servlet container, they're set-up by STC. The mapping and form parameters however, are created by STC by reading the struts-config file.

One of the things you'll soon get curious about is how well STC can simulate the request and response objects. The answer is: pretty well, but it doesn't do the job 100% perfectly. We'll revert to this topic later.    

Let's have a look at a simple STC testprogram:

Listing 1: A StrutsTestCase program
package hansen.playground;

import servletunit.struts.*;

public class TestStrutsAction extends MockStrutsTestCase {

  public void setUp() throws Exception { super.setUp(); }
  public void tearDown() throws Exception { super.tearDown(); }
  public TestStrutsAction(String testName) { super(testName); }

  public void testList() {
    setRequestPathInfo("/list");
    actionPerform();
    verifyForward("list");
    verifyNoActionErrors();
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestStrutsAction.class);
  }
}

The class structure will be familiar to JUnit users:

MockStrutsTestCase extends JUnit's TestCase class, so all of the "assert" methods from JUnit are available in the test methods.

What's not familiar are the four statements in the testList method. Let's have a closer look at them:

statement purpose
setRequestPathInfo("/list")
tells STC that we want to test the "list" action defined in struts-config. Note that struts-config contains the information about which class to call  
actionPerform()
tells STC to make the call to the execute method 
verifyForward("list")
tells STC to verify that a forward to the name "list" was attempted. "list" is also defined in struts-config 
verifyNoActionErrors()
tells STC to verify that the execute method didn't create any ActionError objects. 

The snippet from the struts-config file could look like this:

<action   path="/list"
          type="hansen.playground.ListDVDAction">
    <forward name="list" path="/list.jsp"/>
</action>

I hope you appreciate the simplicity of the STC set-up. It's no big deal to get started testing your Struts actions, so what we need now is a Struts application and a download of STC. Let's take the application first.

A demo application

I admit I've used the DVD library application before: for example in my article "Coding your second Jakarta Struts Application". It's a classical list-detail application and it's great for demonstrations since it's so simple, still it illustrates many important aspects of a web application. It has these two pages:

Figure 2: The List page    

Figure 3: The Detail page    

Here are the Struts actions used to navigate among the two screens:

Figure 4: Actions for the DVD application    

The DVDs are held in an XML-file (dvds.xml) placed in the application root directory. To read this file I've used JDOM. To better understand the following examples you should know that the list action instantiates a module called DVDManager which reads this xml file and stores it as a JDOM structure in session scope. The other actions may then fetch DVD information through simple calls to the DVDManager.   

The application is started with the list action. On a Tomcat server you'd enter this address in your browser:

http://localhost:8080/dvdlib/list.do

Installing the DVD application and STC

To run the application and the STC programs you need these components:

To ease the process of getting these components I've packed them all as a war-file. Place it in your servlet container, start the container, and the application is running. NOTE: the size of the file is  1.2 MB.

If you already have Struts 1.1, and maybe some of the other components as well, you may download this zip-file containing only the application-specific files (jsp, web.xml, struts-config, application class-files etc.). Size is only 26 KB. 

If you need to download some of the other components use these links:

StrutsTestCase versions

The current release of STC is 2.0. Note, that there is a version for servlet specifications 2.2 and 2.3, and you should pick the version that corresponds to the level supported by your web server. The war-file mentioned above includes the 2.3-version. The STC download contains, first of all, an STC jar-file which you must place in your classpath. The rest of the download is documentation (including JavaDoc) and examples.

Here's a Windows bat-file, setcp.bat,  which you can use for setting up the proper classpath for compiling and running an STC program testing the DVD Library application. The setup assumes Tomcat, but it should be possible to use it with any servlet container, after making minor modifications:    

Listing 2: setcp.bat - a Windows bat-file that sets the correct classpath 
@echo off
set TOMCATROOT=d:\apache\jakarta-tomcat-4.1.12\
set APPDIR=%TOMCATROOT%\webapps\dvdlib
set LIBDIR=%APPDIR%\web-inf\lib\

set CP=.
set CP=%CP%;%TOMCATROOT%common\lib\servlet.jar
set CP=%CP%;%APPDIR%
set CP=%CP%;%LIBDIR%struts.jar
set CP=%CP%;%LIBDIR%strutstest-2.0.0.jar
set CP=%CP%;%LIBDIR%junit.jar

rem Remove the 'rem' if jdk1.4 is NOT used
rem set CP=%CP%;%TOMCATROOT%common\lib\xerces.jar

rem JDOM is used by the DVDManager class
set CP=%CP%;%TOMCATROOT%common\lib\jdom.jar

set CLASSPATH=%CP%
set CP=

To use it you must: 

To simplify things I place my STC test programs in the same directory as the dvdlib application (WEB-INF/classes/hansen/playground). If the setcp bat-file is placed in the classes directory you can compile and run an STC program with this recipe (on Windows):

Table 1: The recipe for compiling and running STC programs
  • open a DOS-window
  • go to the classes directory
  • issue the command "setcp" 
  • issue the commmand "set" to verify thet setcp worked as it should
  • to compile enter "javac hansen\playground\<name_of_testprogram>.java
  • to run the compiled program enter "java hansen.playground.<name_of_testprogram>

We're now ready to run our first STC program, but first a quick recap. STC tests your Struts actions, and the place where they're defined are in the struts-config file.    

The struts-config file for the DVD library application

The part of the struts-config file where the actions are defined looks like this:

Listing 3: Action definitions in struts-config
<action-mappings>

    <action   path="/list"
              type="hansen.playground.ListDVDAction">
        <forward name="list" path="/list.jsp"/>
    </action>

    <action   path="/detail"
              type="hansen.playground.DetailDVDAction"
              name="detailForm"
              validate="false"
              scope="request">
        <forward name="detail" path="/detail.jsp"/>
    </action>

    <action   path="/save"
              type="hansen.playground.SaveDVDAction">
        <forward name="list" path="/list.jsp"/>
    </action>

    <action   path="/process"
              type="hansen.playground.ProcessDVDAction"
              name="detailForm"
              scope="request"
              validate="false">
        <forward name="create" path="/create.do"/>          
        <forward name="update" path="/update.do"/>          
        <forward name="delete" path="/delete.do"/>          
        <forward name="cancel" path="/cancel.do"/>          
    </action>

    <action   path="/create"
              type="hansen.playground.CreateDVDAction"
              name="detailForm"
              input="/detail.jsp"
              scope="request">
        <forward name="OK" path="/detail.jsp"/>          
    </action>

    <action   path="/update"
              type="hansen.playground.UpdateDVDAction"
              name="detailForm"
              input="/detail.jsp"
              scope="request">
       <forward name="OK" path="/detail.jsp"/>          
    </action>

    <action   path="/delete"
              type="hansen.playground.DeleteDVDAction"
              name="detailForm"
              scope="request"
              validate="false">
        <forward name="OK" path="/list.jsp"/>          
    </action>

    <action   path="/cancel"
              forward="/list.jsp">
    </action>

</action-mappings>

We also need to know the global forwards:

<global-forwards>
    <forward name="error" path="/error.jsp"/>
</global-forwards>

Running the first testprogram

The testprogram from  Listing 1 above is precisely what we need to test the list action. So let's use the recipe from Table 1. Place the testprogram in classes/hansen/playground, compile it, and run it. 

The result however, is rather disappointing:

. . .
Path:null
F
Time: 1,38
There was 1 failure:
1) testList(hansen.playground.TestStrutsAction)
  junit.framework.AssertionFailedError: 
was expecting '/list.jsp' but received '/error.jsp'
  at servletunit.struts.Common.verifyForwardPath(Common.java:313)
  at servletunit.struts.MockStrutsTestCase.verifyForward(
    MockStrutsTestCase.java:605)
  at hansen.playground.TestStrutsAction.testList(
    TestStrutsAction.java:14)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(
    NativeMethodAccessorImpl.java:39)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(
    DelegatingMethodAccessorImpl.java:25)
  at hansen.playground.TestStrutsAction.main(TestStrutsAction.java:19)

FAILURES!!!
Tests run: 1,  Failures: 1,  Errors: 0

What we see is that the expected forward, list (which points to list.jsp), was not used. In stead the error forward (from the global-forwards section) was selected by the ListDVDAction class. If we look in the source code for this class, we can see that this can happen only if the dvds.xml file was not found. Here's the code that finds the absolute path for this file:

public ActionForward execute(ActionMapping mapping,
				 ActionForm form,
				 HttpServletRequest request,
				 HttpServletResponse response) {

    HttpSession session = request.getSession();
    ServletContext cont = session.getServletContext();
    . . .    
    String filename = cont.getRealPath("dvds.xml");
    System.out.println("Path:"+filename);
 
    if (filename == null || !new File(filename).exists()) {
    . . . (ends up in forwarding to "error")

In the output from the run above, you can see that the file's pathname was "null". What we are facing here is a situation where the mock object, MockStrutsTestCase, doesn't simulate the servlet environment correctly. It's the getRealPath method that returns the null value, but there is a way around this problem. If you look at the JavaDoc for the MockStrutsTestCase, you'll see that it has several methods for setting environment info--for example the value for getRealPath:

setContextDirectory(java.io.File contextDirectory)
          Sets the context directory to be used with the getRealPath() methods in the ServletContext and HttpServletRequest API.    

We'll therefore add the path where the dvds.xml file is placed:

. . .
import java.io.*;
. . .
public void testList() {
   setContextDirectory(new File(
      "d:\\apache\\jakarta-tomcat-4.1.12\\webapps\\dvdlib"));
   setRequestPathInfo("/list");
. . .

We compile and run the testprogram again:

. . .
Path:d:\apache\jakarta-tomcat-4.1.12\webapps\dvdlib\dvds.xml
18-07-2003 20:12:13 servletunit.ServletContextSimulator log
INFO: ActionServlet: 
Reading dvds from file: 
d:\apache\jakarta-tomcat-4.1.12\webapps\dvdlib\dvds.xml
18-07-2003 20:12:13 servletunit.ServletContextSimulator log
INFO: ActionServlet: Reading dvds.xml: 3 dvds read

Time: 1,65

OK (1 test)

The path info is written by a System.out.println call in the Action class. The two messages starting with a timestamp comes from STC's simulation of the servlet.log method. "Time" and "OK" comes from JUnit.  

I do hope that this example didn't scare you off. It's not very often that you need to fiddle around with the environment settings. On the other hand: in real life projects you'll always meet technical issues like this, and it's important to know how you may resolve them. So my first tip is: get familiar with the STC JavaDoc.

The next test program

The way you organize JUnit programs is that you create many test-methods, each testing a specific feature. We'll therefore add a new method to test the "detail" action, which brings us from the List page to the Detail page:

public void testDetail() {
    setRequestPathInfo("/detail");
    addRequestParameter("index","1");
    actionPerform();
    verifyForward("detail");
    verifyNoActionErrors();
}

The detail action needs to carry a parameter holding the index for the DVD to be detailed. In the web application this is done with a hyperlink:

http://localhost:8080/dvdlib/detail.do?index=1

So you use the addRequestParameter method to add parameters to the request. The index value could as well come from an HTML form field. The Action class would not be able to tell the difference. 

If we run this program we're once again surprised:

. . .
F
Time: 1,97
There was 1 failure:
1) testDetail(hansen.playground.TestStrutsAction)
junit.framework.AssertionFailed
Error: was expecting '/detail.jsp' but received '/error.jsp'
   at servletunit.struts.Common.verifyForwardPath(Common.java:313)
. . .
FAILURES!!!
Tests run: 2,  Failures: 1,  Errors: 0

Looking in the code for the DetailDVDAction class we can see there are two cases where we'll forward to error.jsp: either the index value is not given (and we didn't forget that) or the DVD collection stored in session scope by the list action is null. The answer to the failure above is that STC works like JUnit: each test method starts from scratch by calling the setUp method, and you can not pass data from one test method to another. 

What we could do was to let the setUp method create the DVD collection and save it in session scope, but I'll use the simpler approach where we use one single test method. If the list action is called first, everything should be OK. Let's try that:

public void testActions() {
  setContextDirectory(new File(
    "d:\\apache\\jakarta-tomcat-4.1.12\\webapps\\dvdlib"));
  setRequestPathInfo("/list");
  actionPerform();
  verifyForward("list");
  verifyNoActionErrors();

  setRequestPathInfo("/detail");
  addRequestParameter("index","1");
  actionPerform();
  verifyForward("detail");
  verifyNoActionErrors();
}

Now the test shows no errors:

. . .
INFO: ActionServlet: Reading dvds.xml: 3 dvds read

Time: 2,74

OK (1 test)

Testing error situations

The failures we've encountered should remind us that every test program must also test the error situations. Luckily STC can handle this situation as well. I mentioned that the detail action needs an index value to function, and if we omit it we'll get an error as seen above:

. . .
F
Time: 1,6
There was 1 failure:
1) testActions(hansen.playground.TestStrutsAction)
junit.framework.AssertionFailedError:
was expecting '/detail.jsp' but received '/error.jsp'

Since the DetailDVDAction class forwards to error in two situations, we'll have to check which value was given to the Struts ActionError method. The test for a missing index sends the value "error.noindex": 

. . .
// Test the detail action - without an index
    setRequestPathInfo("/detail");
    actionPerform();
    verifyActionErrors(new String[] {"error.noindex"});

When we run the test program once more we receive no error messages.

If you continue your program with a new test in the same test-method then you should realize that the request object now contains a non-null ActionErrors object. If you don't clear it the next test using verifyNoActionErrors() will fail. To clear it, insert this statement in your code:

getRequest().setAttribute(Globals.ERROR_KEY,null);
The Globals.ERROR_KEY may be imported from org.apache.struts.Globals. 

See the complete test program here .

Conclusion

An important part of unit testing a Struts application is to test if the execute method of the Action classes work properly. STC sets up a simulated Struts environment that makes it very simple to code these tests. Since STC uses JUnit your test programs will fit nicely into the collections of unit tests you probably already have made for your application.

I've shown you how to code and run STC programs, and you've also seen that there may be some pieces missing in the simulated environment. A way to get an idea of what will work and what might not work is to browse through the STC JavaDoc. If you, for example, spend some time looking at the JavaDoc for the MockStrutsTestCase, you'll also see that there are features which we haven't covered yet.

In the next article we'll see how easy it is to replace the mock object environment with the Cactus framework, thereby using the real servlet environment set up by the web server. We'll also see how to handle the "setup once for all method calls" situation in an elegant way, and there'll be some other goodies too.            

Resources