StrutsTestCase (STC) is a framework for testing Struts Action classes. It uses JUnit and in my first article about StrutsTestCase (STC) we looked at how STC could use a "mock object approach", where the servlet and Struts environments are simulated. STC also offers another testing possibility based on the Cactus framework, where the test is carried out in a real web server environment.
The main topic of the current article will be about The Cactus option, but first I'd like to touch upon a couple of important features pertaining to the mock object option.
The MockStrutsTestCase class has methods that'll give you access to the simulated objects. These are:
Table 1: Accessing the simulated objects
Object | Method in MockStrutsTestCase |
HttpServletRequest | getRequest() |
HttpServletResponse | getResponse() |
HttpSession | getSession() |
ActionForm (Struts) | getActionForm() |
ActionServlet (Struts) | getActionServlet() |
Having these objects you may inspect or modify them just as you're used to. But since they're simulated objects, STC also gives you the option to modify some of the properties which are read-only in the real Servlet and Struts APIs. So if the simulation of an object is not quite all right, you should check if it's possible for you to set the property. STC makes this possible through a set of classes all ending with "Simulator" (cfr. the STC JavaDoc). Following are a few examples.
A browser sends, along with the file request, a set of HTTP headers which you may access in the Servlet API through the getHeader and getHeaderNames methods. The MockStrutsTestCase doesn't initialize any header data, so if your servlet needs data from the headers (it could for example be the name of the browser), you code like this:
HttpServletRequestSimulator req = (HttpServletRequestSimulator)getRequest(); req.setHeader("user-agent", "Mozilla/4.0"); |
So the trick is to cast to a Simulator class, and then use its set-methods.
If you have a security set up with defined user-roles, it's nice that you may set the role like this:
HttpServletRequestSimulator req = (HttpServletRequestSimulator)getRequest(); req.setUserRole("admin"); |
These lines must of course come before the call to actionPerform().
The servlet context contains information about the servlet container and data from the web.xml file. Let's see how STC identifies itself:
ServletContext ctx = getActionServlet().getServletContext(); System.out.println(ctx.getServerInfo()); System.out.println ("Supports Servlet API " + ctx.getMajorVersion() + "." + ctx.getMinorVersion()); |
The output from this bit of code is:
MockServletEngine/1.9.5 Supports Servlet API 2.3 |
It's simple to add a parameter to the servlet context. Again we use a simulator class:
ServletContextSimulator context = (ServletContextSimulator)ctx; context.setInitParameter("parm1", "value1"); |
Among the files you may download, there's a program called MockStrutsTestCaseSimulation that gives several examples on how to modify the simulated objects.
A common "problem" when you're coding test cases for Struts actions is that you need some kind of initializations to be carried out before you can start your testing. JUnit has the setUp method for this purpose, but it's called for every test-method in your test class. This may add some unwanted overhead to your program, and there is a way around the problem.
Use a JUnit suite method with an inner class construction like this (the class name is MockOneTimeSetup):
public static Test suite() { TestSuite suite = new TestSuite(MockOneTimeSetup.class); TestSetup wrapper = new TestSetup(suite) { public void setUp() { // code your set up here } public void tearDown() { // code your tear down here } }; return wrapper; } |
It's important to note, however, that the suite method is static. So if you want to save some data, use static variables and fetch them from the ordinary JUnit setUp method. Here's an example where a list of DVDs are fetched once, saved in static storage, and then--for each textXXX-method--saved in session scope:
private static DVDManager dvds; public void setUp() throws Exception { super.setUp(); getSession().setAttribute("dvds", dvds); } public static Test suite() { TestSuite suite = new TestSuite(MockOneTimeSetup.class); TestSetup wrapper = new TestSetup(suite) { public void setUp() { dvds = new DVDManager(); } public void tearDown() { // nothing } }; return wrapper; } . . . |
Cactus is yet another useful open source system from the Jakarta project. Its purpose is--and I quote from the Cactus web site:
Cactus is a simple test framework for unit testing server-side java code (Servlets, EJBs, Tag Libs, Filters, ...). The intent of Cactus is to lower the cost of writing tests for server-side code. It uses JUnit and extends it. Cactus implements an in-container strategy.
So STC uses Cactus by building on its servlet features, more specifically the ServletTestCase class.
You may ask: why offer two testing techniques--the mock object approach and the in-container approach--if one is enough? The answer is: both strategies have their pros and cons. As I see it, the primary advantage by using the mock approach is its simplicity. As was shown in my first article it's very easy to get the test set up ready, and you're not dependent on having a servlet environment available. The advantage by the in-container approach is, of course, that the test is carried out in the real servlet environment. This improves the chances for having a production system that will run your Action classes without errors.
It you're interested in more details on the two testing techniques then look at this article on the Cactus web site.
Before we can use Cactus from STC we'll have to download it from the Jakarta web site (look for "Release builds"). The size is about 5Mb.You may also choose to download the war-file with all the examples from this article. It's 1.6 Mb, but then you don't get documentation and samples.
The download from the Jakarta site comes in two flavors--one for J2EE 1.2 and one for J2EE 1.3. Choose the one that is supported by your servlet container. I'll be using Tomcat version 4 in my examples, and it supports J2EE 1.3. Cactus is currently on version 1.5-beta1.
The way Cactus must be installed, so STC can use it properly, is described in the STC download file examples/README.txt. Installation can be a little tricky, so I'll go through it step by step. If you don't have patience for this you might simply use the war-file: drop it in your web container, restart the web server and you're ready for running the tests.
But some of us like to dig down in the details, so let's do just that. First of all you should realize that Cactus uses the same client-server set-up as when you use a browser against a web server. This means that you'll have to consider the classpath for the client as well as the web server. A key thing to understand is that your test program must be on the client side as well as the server side:
Figure 1: The Cactus architecture
The client copy of MyTestCase--the STC/Cactus/JUnit test program--issues an HTTP request to the web server, where it's handled by a Cactus servlet (The Redirector Proxy), which then invokes the server copy of MyTestCase. It's on the server side that setUp and testXXX methods are called. A common mistake is to make corrections to MyTestCase and forget to deploy it twice!
To simplify the set up, I'll only use one copy of the test program. So as client I use the server copy in the WEB-INF/classes directory on my Tomcat server. The client may then also share the jar-files from the servers WEB-INF/lib directory. If you're interested in all the details of what jar-files are needed by the client and which ones by the web server, then look at the Cactus documentation on this subject.
Step 1: Make a copy of the "DVD library" application from my first article: copy the dvdlib directory and call it "cactusdvdlib".
Step 2: Copy three jar-files from the Cactus download's lib directory to the cactusdvdlib/WEB-INF/lib directory. The files are:
cactus.jar commons-httpclient.jar aspectjrt.jar
(I've shortened the file names, which also contain release numbers).
Warning: In the Cactus documentation for installation on Tomcat, it's recommended to put the jar files in the Tomcat's common library: common/lib. If you do this then be prepared for strange classloader problems--e.g. missing classes, even when they seem to be in the classpath. I've not been able to use common/lib with Tomcat 4.1.12 for the examples in this article.
Step 3: Add four jar-files to the setcp bat file from my first article: cactus, commons-httpclient, commons-logging, and aspectjrt. The setcp file is placed in the classes directory, and it defines the classpath for the client test program.
Listing 1: setcp.bat - a Windows bat-file that sets the classpath for the client
@echo off set TOMCATROOT=d:\apache\jakarta-tomcat-4.1.12\ set APPDIR=%TOMCATROOT%\webapps\cactusdvdlib 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 set CP=%CP%;%LIBDIR%cactus.jar set CP=%CP%;%LIBDIR%aspectjrt.jar set CP=%CP%;%LIBDIR%commons-httpclient.jar set CP=%CP%;%LIBDIR%commons-logging.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= |
Note that TOMCATROOT must point to the directory where your Tomcat server is installed.
Step 4: Add the Cactus configuration file cactus.properties to cactusdvdlib/WEB-inf/classes (then it'll be in the classpath defined in step 3):
Listing 2:
# Configuration file for Cactus. cactus.contextURL = http://localhost:8080/cactusdvdlib cactus.servletRedirectorName = ServletRedirector |
Note that we assume the server is running on port 8080 (as Tomcat does as default). The URL is used by the STC test program when it sends its request to the web server, and it must contain the name of the directory for the project.
Step 5: Add two Cactus servlet definitions to your web-xml file. The ServletRedirector is the program that receives the servlet request from your test program and executes the server side code. The ServletTestRunner may be used to run your test program from a browser.
Listing 3:
. . . <servlet> <servlet-name>ServletRedirector</servlet-name> <servlet-class> org.apache.cactus.server.ServletTestRedirector </servlet-class> </servlet> <servlet> <servlet-name>ServletTestRunner</servlet-name> <servlet-class> org.apache.cactus.server.runner.ServletTestRunner </servlet-class> </servlet> . . . <servlet-mapping> <servlet-name>ServletRedirector</servlet-name> <url-pattern>/ServletRedirector</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ServletTestRunner</servlet-name> <url-pattern>/ServletTestRunner</url-pattern> </servlet-mapping> . . . |
To see if the ServletRedirector servlet is working, enter
http://localhost:8080/cactusdvdlib/ServletRedirector? Cactus_Service=RUN_TEST
If you get a blank page back, the servlet works!
Step 6: Use the class CactusStrutsTestCase instead of MockStrutsTestCase in your test program. Let's use the test program from the first article:
Listing 4:
package hansen.playground; import servletunit.struts.*; import java.io.*; import org.apache.struts.*; public class TestStrutsAction extends CactusStrutsTestCase { public void setUp() throws Exception { super.setUp(); } public void tearDown() throws Exception { super.tearDown(); } public TestStrutsAction(String testName) { super(testName); } public void testActions() { // Test the list action setRequestPathInfo("/list"); actionPerform(); verifyForward("list"); verifyNoActionErrors(); . . . } public static void main(String[] args) { junit.textui.TestRunner.run(TestStrutsAction.class); } } |
Compile the program (using the setcp.bat file from Listing 1), start or restart your web server and run the test program.
If everything works it will produce an output like this:
D:...cactusdvdlib\WEB-INF\classes> java hansen/playground/TestStrutsAction . Time: 2,56 OK (1 test) |
Errors, either when compiling or running the test program, are probably because of incorrect classpath setup. Check the spelling of files in the setcp.bat file, and check that you didn't forget any of the 6 steps above. If the error is still there, deploy your test case to both the client and the server targets, and restart your server. If nothing helps then check the Cactus FAQ and the STC FAQ.
Did you notice the time taken to run the test program above? 2,56 seconds is considerably more than when we used the mock object set up. Cactus has a much more complex set up--you pay for what you get--a more realistic test.
Now add a JUnit suite method to the test program--like this:
import junit.framework.*; . . . public static Test suite() { return new TestSuite(CactusTestStrutsAction.class); } . . . |
When it's compiled, you'll be able to run the test program from your browser. Enter this address in your browser:
http://localhost:8080/cactusdvdlib/ServletTestRunner? suite=hansen.playground.TestStrutsAction
The result will look like this:
Figure 2: Output from ServletTestRunner
You might think that the one-time setup for JUnit explained earlier will work just as fine with Cactus. The answer is: Not so. Remember that two copies of the test program are active. When you start the test case, the client copy executes the static suite method, but this code was supposed to run on the server side, so it might fail. Even if it doesn't we still have a problem, because the server copy will not execute the suite method. So when the setUp method is called on the server side, it won't have any static data to access.
Unfortunately I don't have a solution for Cactus to the one-time set up case, but if you have one, please mail me!
The Cactus set up can not use the simulated objects from the mock approach (table 1). Instead you may use these properties on the Cactus class ServletTestCase, which CactusStrutsTestCase inherits from:
Table 2: Accessing Cactus wrappers
Property in ServletTestCase | Returns this object |
request | HttpServletRequestWrapper |
config | ServletConfigWrapper |
The two objects offer a few settable properties, for example:
this.request.setRemoteUser("admin");
Read more about the wrappers here.
Let's see how the Cactus client presents itself to the web server. If we run a slightly modified version of the previous class--now called CactusStrutsTestCaseSimulation --we can see the server name and the HTTP headers:
Apache Tomcat/4.1.12-LE-jdk14 Supports Servlet API 2.3 . . . =========Headers: Name: content-type, value: application/x-www-form-urlencoded Name: user-agent, value: Jakarta Commons-HttpClient/2.0beta2 Name: host, value: localhost:8080 |
The server name is correct, and the user-agent (normally the browser name) shows that Cactus is using the HttpClient from the Jakarta project.
What if the servlet we're testing depends in some way upon header information which isn't set by the Cactus client? An example is userid and password entered using BASIC authentication. Well, just like the server copy of the test case has its setUp and tearDown methods, the client copy has a beginXXX and endXXX method--for every testXXX method. The signature of the beginXXX method is:
void beginXXX(WebRequest theRequest)
where WebRequest is a Cactus class with several useful setter-methods. One is setAuthentication, which may set the userid and password for BASIC authentication. Here's a small test program that shows how to log on to a web application with userid and password = "tomcat":
Listing 4: Program for authetication test
package hansen.playground; import org.apache.cactus.WebRequest; import org.apache.cactus.client.authentication.BasicAuthentication; import servletunit.struts.CactusStrutsTestCase; public class CactusAuthentication extends CactusStrutsTestCase { public static void main(String[] args) { junit.textui.TestRunner.run(CactusAuthentication.class); } public void setUp() throws Exception { super.setUp(); } public void tearDown() throws Exception { super.tearDown(); } public CactusAuthentication(String testName) { super(testName); } public void beginAuth(WebRequest theRequest) { theRequest.setAuthentication( new BasicAuthentication("tomcat", "tomcat")); } public void testAuth() { assertEquals("tomcat", request.getUserPrincipal().getName()); assertEquals("tomcat", request.getRemoteUser()); assertTrue("User not in 'tomcat' role", request.isUserInRole("tomcat")); } } |
For this example to work you'll have to define a security set up on your server. On Tomcat you could add this to the web.xml file:
Listing 5: Security definitions in web.xml
<security-constraint> <web-resource-collection> <web-resource-name>The Entire Web Application</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>tomcat</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> </login-config> <security-role> <role-name>tomcat</role-name> </security-role> |
The role "tomcat" must be defined in the file tomcat-users.xml in the server's conf directory.
If you'd like to read more about security set ups go to the Cactus site and read " Introduction to testing secure code ". You may also find this information in the Cactus download.
With these two articles about STC, I hope I've convinced you that testing Struts Action classes is not too difficult. The mock object option is the easiest way to get acquainted with STC, and in many cases this option is sufficient to have your code thoroughly tested. The Cactus option works with your chosen web server, and therefore gives you some extra features, like authentication test, which can be very valuable.
Like any other part of your program Action classes need to be tested, right? If you want to deliver quality code--and who doesn't?--then you need to test your code. Don't forget: real programmers love writing tests!