ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Test-Driven Development Using StrutsTestCase

by John Ferguson Smart
10/26/2005

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.

Typical J2EE architecture
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:

These are typical benefits of test-driven development, and they are as applicable in the Struts action layer as anywhere else.

Introducing StrutsTestCase

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.

Programming Jakarta Struts

Related Reading

Programming Jakarta Struts
By Chuck Cavaness

StrutsTestCase in Practice

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:

If we were using tiles, we could also check that the "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);
  ...
}

Testing Struts Error Handling

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.

Customizing the Test Environment

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");
}

First-Level Performance Testing

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)
}

Conclusion

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.

Resources

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.