StrutsTestCase is a powerful and easy-to-use testing framework for Struts actions. Using Struts and then StrutsTestCase, in combination with traditional JUnit tests, will give you a very high level of test coverage and increase your product reliability accordingly.
StrutsTestCase is a test framework based on JUnit for testing Struts actions. If you use Struts, it can provide an easy and efficient manner for testing the Struts action classes of your application.
Typical J2EE applications are built in layers, as illustrated in Figure 1.

Figure 1. Typical J2EE architecture
The DAO and business layers can be tested either using classic JUnit tests or some of the various JUnit extensions, depending on the architectural details. DbUnit is a good choice for database unit testing--see Andrew Glover's "Effective Unit Testing with DbUnit" for more on DbUnit.
On the other hand, testing Struts actions has always been difficult. Even when business logic is well confined to the business layer, Struts actions generally contain important data validation, conversion, and flow control code. Not testing the Struts actions leaves a nasty gap in code coverage. StrutsTestCase lets you fill this gap.
Unit testing the action layer also provides other benefits:
The StrutsTestCase project
provides a flexible and convenient way to test Struts actions from
within the JUnit framework. It lets you do white-box testing on
your Struts actions by setting up request parameters and checking
the resulting Request or Session state after the action has been
called.
StrutsTestCase allows either a mock-testing approach, where the framework simulates the web server container, or an in-container approach, where the Cactus framework is used to run the tests from within the server container (for example, Tomcat). In general, I prefer the mock-testing approach because it is more lightweight and runs faster, and thus allows a tighter development cycle.
All StrutsTestCase unit test classes are derived from either
MockStrutsTestCase for mock testing, or from
CactusStrutsTestCase for in-container tests. We'll
concentrate on mock testing here, as it requires less setup and is
faster to run.
|
Related Reading Programming Jakarta Struts |
|
To test this action using StrutsTestCase, we create a new class
that extends the MockStrutsTestCase class. This class
provides methods to build a simulated HTTP request, to call the
corresponding Struts action, and to verify the application state
once the action has been completed.
Imagine an online accommodation database with a multi-criteria
search function. The search function is implemented by the
/search.do action. The action will perform a
multi-criteria search based on the specified criteria and places
the result list in a request-scope attribute named results. For
example, the following URL should display a list of all
accommodation results in France:
/search.do?country=FR
Now suppose we want to implement this method using a test-driven approach. We write the action class and update the Struts configuration file. We also write the test case to test the (empty) action class. Using a strict test-driven development approach, we write the test case first, and then implement the code to match the test case. In practice, the exact order may vary depending on the code to be tested.
The initial test case will look like this:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
}
Here we set up the path to call
(setRequestPathInfo()) and add a request parameter
(addRequestParameter()). Then we invoke the action
class with actionPerform(). This will verify the
Struts configuration and call the corresponding action class, but
will not test what the action actually does. To do that, we need
to verify the action results.
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
}
Here we check three things:
ActionError messages
(verifyNoActionErrors())."success" forward was returned.results attribute was placed in the
request scope."success"
forward actually points to the right tiles definition, using
verifyTilesForward():
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyTilesForward("success",
"accommodation.list.def");
assertNotNull(request.getAttribute("results"));
}
|
In practice, we will probably want to perform business-specific
tests on the test results. For instance, suppose the
results attribute is expected to be a
List containing a list of exactly 100
Hotel domain objects, and that we want to be sure that
all of the hotels in this list are in France. To do this type of test,
the code will be very similar to standard JUnit testing:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results
= (List) request.getAttribute("results");
assertEquals(results.size(), 100);
for (Iterator iter = results.iterator();
iter.hasNext();) {
Hotel hotel = (Hotel) iter.next();
assertEquals(hotel.getCountry,
TestConstants.FRANCE);
...
}
}
When you test more complex cases, you may want to test sequences of
actions. For example, suppose the user does a search on all hotels
in France, and then clicks on an entry to display the details.
Suppose we have a Struts action to display the details of a given
hotel, which can be called as follows:
/displayDetails.do?id=123456
Using StrutsTestCase, we can easily simulate a sequence of actions in the same test case, where a user performs a search on all hotels in France, and then clicks on one to see the details:
public void testSearchAndDisplay() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results
= (List) request.getAttribute("results");
assertEquals(results.size(),100);
Hotel hotel = (Hotel) results.get(0);
setRequestPathInfo("/displayDetails.do");
addRequestParameter("id", hotel.getId());
actionPerform();
verifyNoActionErrors();
verifyForward("success");
Hotel hotel
= (Hotel)request.getAttribute("hotel");
assertNotNull(hotel);
...
}
It is also important to test error handling. Suppose we want to
check that the application behaves gracefully if an illegal country
code is specified. We write a new test method and check the
returned Struts ErrorMessages using
verifyActionErrors():
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors(
new String[] {"error.unknown,country"});
verifyForward("failure");
}
Sometimes you want to verify data directly in the ActionForm
object. You can do this using getActionForm(), as in
the following example:
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors(
new String[] {"error.unknown,country"});
verifyForward("failure");
SearchForm form = (SearchForm) getActionForm();
assertEquals("Scott", form.getCountry("XX"));
}
Here, we verify that the illegal country code is correctly kept in
the ActionForm after an error.
|
It is sometimes useful to override the setUp()
method, which lets you specify non-default configuration options.
In this example, we use a different struts-config.xml file and
deactivate XML configuration file validation:
public void setUp() {
super.setUp();
setConfigFile("/WEB-INF/my-struts-config.xml");
setInitParameter("validating","false");
}
Testing an action or a sequence of actions is an excellent way of testing that request response times are acceptable. Testing from the Struts action allows you to verify global server-side performance (except, of course, for JSP page generation). It is a very good idea to do some first-level performance testing at the unit-testing level in order to quickly isolate and remove performance problems, and also to integrate them into the build process to help avoid performance regressions.
Here are some basic rules of thumb that I use for first-level Struts performance testing:
Some open source libraries exist to help with performance testing, such as JUnitPerf by Mike Clark. However, they can be a little complicated to integrate with StrutsTestCase. In many cases, a simple timer can do the trick. Here is a very simple but efficient way of doing first-level performance testing:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
long t0 = System.currentTimeMillis();
actionPerform();
long t1 = System.currentTimeMillis() - t0;
log.debug("Country search request processed in "
+ t1 + " ms");
assertTrue("Country search too slow",
t1 >= 100)
}
Unit testing is an essential part of agile programming in general, and test-driven development in particular. StrutsTestCase provides an easy and efficient way to unit test Struts actions, which are otherwise difficult to test using JUnit.
John Ferguson Smart is a freelance consultant specializing in Enterprise Java, Web Development, and Open Source technologies, currently based in Wellington, New Zealand.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.