Chapter 13. Testing a Struts Application with StrutsTestCase

Introduction

In this chapter, we will look at tools that can improve test quality and efficiency when you are working with Struts applications.

Struts is a popular, widely used and well-documented J2EE (Java 2 Platform, Enterprise Edition) application framework with a long history and an active user community. It is based on a Model-View-Controller (MVC) architecture, which separates an application into (at least) three distinct layers. The Model represents the application business layer, the View represents the presentation layer (in other words, the screens), and the Controller represents the navigational logic, that binds the screens to the business layer. In Struts, the Controller layer is primarily implemented by Action classes, which we will see a lot more of later in this chapter.

Testing user interfaces has always been one of the trickiest parts of testing a web application, and testing Struts user interfaces is no exception to this rule. If you are working on a Struts application, StrutsTestCase is a powerful and easy-to-use testing framework that can make your life a lot easier. 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.

Note that StrutsTestCase does not let you test the HTML or JSP parts of your user interface: you need a tool such as Selenium for that (see Testing Your Web Application with Selenium). StrutsTestCase allows you to test the Java part of your user interface, from the Struts actions down. StrutsTestCase is an open source testing 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.

Testing a Struts Application

Typical J2EE applications are built in layers (as illustrated in Figure 13-1):

  • The DAO layer encapsulates database access. Hibernate mapping and object classes, Hibernate queries, JPA, entity EJBs, or some other entity-relation persistence technology may be found here.

  • The business layer contains more high-level business services. Ideally, the business layer will be relatively independent of the database implementation. Session EJBs are often used in this layer.

  • The presentation layer involves displaying application data for the user and interpreting the user requests. In a Struts application, this layer typically uses JSP/JSTL pages to display data and Struts actions to interpret the user queries.

  • The client layer is basically the web browser running on the user’s machine. Client-side logic (for example, JavaScript) is sometimes placed here, although it is hard to test efficiently.

A typical J2EE architecture
Figure 13-1. A 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 Chapter 14).

Testing the presentation layer in a Struts application 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. By contrast, not testing the Struts actions leaves a nasty gap in your code coverage. StrutsTestCase lets you fill this gap.

Unit testing the action layer also provides other benefits:

  • The view and control layers tend to be better-thought-out and often are simpler and clearer.

  • Refactoring the action classes is easier.

  • It helps to avoid redundant and unused action classes.

  • The test cases help document the action code, which can help when writing the JSP screens.

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

The mock-testing approach is more lightweight and runs faster than the Cactus approach, and thus allows a tighter development cycle. However, the mock-testing approach cannot reproduce all of the features of a full-blown servlet container. Some things are inevitably missing. It is much harder to access server resources or properties, or use JNDI functionality, for example.

The Cactus approach, also known as in-container testing, allows testing in a genuine running servlet container. This has the obvious advantage of simulating the production environment with more accuracy. It is, however, generally more complicated to set up and slower to run, especially if the servlet container has to restart each time you run your tests.

All StrutsTestCase unit test classes are derived from either MockStrutsTestCase for mock testing, or from CactusStrutsTestCase for in-container testing. Here, we will look at both techniques.

Mock Tests Using StrutsTestCase

Mock-testing in StrutsTestCase is fast and lightweight, as there is no need to start up a serlvet container before running the tests. The mock-testing approach simulates objects coming from the web container to give your Action objects the impression that they are in a real server environment.

To test an action using StrutsTestCase, you create a new test class that extends the MockStrutsTestCase class. The MockStrutsTestCase 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 you are asked to write an online accommodation database with a multicriteria search function. According to the specifications, the search function is to be implemented by the /search.do action. The action will perform a multicriteria search based on the specified criteria and places the result list in a request-scope attribute named results before forwarding it to the results list screen. For example, the following URL should display a list of all accommodation results in France:

/search.do?country=FR

To implement this function in Struts, we need to write the corresponding action class and update the Struts configuration file accordingly. Now, suppose that we want to implement this method using a test-driven approach. Using a strict test-driven approach, we would try to write the unit test first, and then write the Action afterward. In practice, the exact order may vary depending on the code to be tested. Here, in the first iteration, we just want to write an empty Action class and set up the configuration file correctly. StrutsTestCase mock tests can check this sort of code quickly and efficiently, which lets you keep the development loop tight and productivity high. The first test case is fairly simple, so we can start here. This initial test case might look like this:

public class SearchActionTest extends MockStrutsTestCase {
    public void testSearchByCountry() {
        setRequestPathInfo("/search.do");
        addRequestParameter("country", "FR");
        actionPerform();
    }
}

StrutsTestCase tests usually follow the same pattern. First, you need to set up the URL you want to test. Behind the scenes, you are actually determining which Struts action mapping, and which action, you are testing. StrutsTestCase is useful for this kind of testing because you can do end-to-end testing, pretty much from the HTTP request through the Struts configuration and mapping files, and down to the Action classes and underlying business logic.

You set the basic URL by using the setRequestPathInfo() method. You can add any request parameters using the addRequestParameter() method. The previous example sets up the URL “/search.do?country=FR” for testing.

When it is doing mock tests, StrutsTestCase does not try to test this URL on a real server: it simply studies the struts-config.xml file to check the mapping and invoke the underlying Action class. By convention, StrutsTestCase expects to find the struts-config.xml file in your WEB-INF directory. If, for some reason, you need to put it elsewhere, you will need to use the setConfigFile() method to let StrutsTestCase know where it is.

Once this is set up, you invoke the Action class by using the actionPerform() method. This creates mock HttpServletRequest and HttpServletResponse objects, and then lets Struts take control. Once Struts has finished running the appropriate Action methods, you should check the mock HttpServletResponse to make sure that the application is now in the state we where expecting. Are there any errors? Did Struts forward to the right page? Has the HttpSession been updated appropriately? And so on. In this simple case, we simply check that the action can be invoked correctly.

In our first iteration, we just want to write, configure, and invoke an empty Struts Action class. The main aim is to verify the Struts configuration. The Action class itself might look like this:

public class SearchAction extends Action {
  /** 
   * Search by country
   */
  public ActionForward execute(ActionMapping mapping,
                               ActionForm form,
                               HttpServletRequest request,
                               HttpServletResponse response) {

    //
    // Invoke model layer to perform any necessary business logic
    //
    ...
    //
    // Success!
    //
    return mapping.findForward("success");
  }
}

We also update the Struts configuration file to use this class when the /search.do URL is invoked. The relevant parts of the struts-config.xml file are shown here:

<struts-config>
  <form-beans>
    ...
    <form-bean name="searchForm" type="org.apache.struts.action.DynaActionForm">
      <form-property name="country" type="java.lang.String" />
    </form-bean>
    ...
  </form-beans>
  <action-mappings>
    ...
    <!--  Search -->
    <action path="/search" attribute="searchForm" name="searchForm" 
            scope="request" type="com.wakaleo.jpt.struts.sample.actions.SearchAction"
            validate="false">
      <forward name="success" path="/pages/SearchResults.jsp" />
      <forward name="failure" path="/pages/Welcome.jsp" />
    </action>
    ...
  </action-mappings>
</struts-config>

This mapping is not particularly complex. We use a simple DynaForm, called “searchForm,” to pass the query parameter to the action. The action itself is implemented by the SearchAction class shown above.

Now, without further ado, we can run our unit test and make sure that everything works. Our sample application uses Maven (see Chapter 2), so we just need to put the unit test class in the appropriate directory and run the tests as follows:

$ mvn test
.
--------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.wakaleo.jpt.struts.sample.actions.SearchActionTest
...
INFO: Initialize action of type: com.wakaleo.jpt.struts.sample.actions.SearchAction
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.866 sec

Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Tue Nov 14 23:16:44 NZDT 2006
[INFO] Final Memory: 4M/11M
[INFO] ------------------------------------------------------------------------

This test will verify the Struts configuration and call the corresponding Action class, which is certainly worth doing. As any Struts developer will confirm, a lot of time can be wasted because of silly errors in the struts-config.xml file. However, it will not check what the action actually does in any detail. To do that, we need to verify the action results.

StrutsTestCase provides many ways to check the outcome of your actions. Some of the principal techniques include the following: you also can inspect the state of the HTTP request and session objects, using getRequest(), getResponse(), and getSession(), respectively.

Error or informational messages

It is often useful to test whether error conditions are being correctly returned to the presentation layer. You can check for the presence (or lack thereof) of action messages returned by the controller using methods such as verifyNoActionErrors() and verifyActionErrors().

Navigation

You can verify that the action has transferred control to a particular page, using either the logical forward name (verifyForward()) or the actual target path (verifyForwardPath()).

Application state

You can also inspect the state of the HTTP request, response, and session objects, using getRequest(), getResponse(), and getSession(), respectively.

Some of these techniques are illustrated in the following test cases:

public class SearchActionTest extends MockStrutsTestCase {
    public void testSearchByCountry() {
        setRequestPathInfo("/search.do");
        addRequestParameter("country", "FR");
        actionPerform();
        verifyNoActionErrors();
        verifyForwardPath("/pages/SearchResults.jsp");
        assertNotNull(request.getAttribute("results"));
    }
}

Here, we check three things:

  • There were no ActionError messages.

  • Control was passed to the “/pages/SearchResults.jsp” page.

  • The results attribute was placed in the request scope.

In this example, we are using simple JSP pages for the presentation layer. Many Struts projects use Tiles, which is a powerful templating system that ties in well with Struts. You can use StrutsTestCase to test your Tiles mappings as well. For example, you could check that the “success” forward actually points to the right tiles definition (in this case, “result.page”), using the verifyTilesForward() method, as follows:

  verifyTilesForward("success", "result.page");

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 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();
  verifyForwardPath("/pages/SearchResults.jsp");
  assertNotNull(request.getAttribute("results"));
  List<Hotel> results = (List<Hotel>) request.getAttribute("results"); 
  assertEquals(results.size(), 100);
  for (Hotel hotel : results) {
       assertEquals(hotel.getCountry(), "France");
  }
}

When you test more complex cases, you may want to test sequences of actions. For example, suppose we do a search on all hotels in France, and then click 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<Hotel> results = (List<Hotel>) request.getAttribute("results"); 
  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

Error handling is an important part of any web application and needs to be tested appropriately. In StrutsTestCase, you can test error handling principally by checking that your actions return the correct messages when something goes wrong. 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 Extensible Markup Language (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 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:

  • Test multicriteria search queries with as many combinations as possible (to check that indexes are correctly defined).

  • Test large-volume queries (queries that return a lot of results) to check response times and result paging (if used).

  • Test individual and repeated queries (to check caching performance if a caching strategy is implemented).

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 instances, 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. However, Struts actions have traditionally been a weak point in the unit testing process and tend to be poorly tested, thus introducing a high risk of bugs and unstable code. StrutsTestCase provides a good solution to this problem. StrutsTestCase is an easy and efficient way to unit test Struts actions, which are otherwise difficult to test using JUnit.

Mock testing is an excellent approach for developers to test their Struts actions, allowing quick testing and fast feedback. The more cumbersome in-container approach can be useful for integration testing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.102.166