Skip to main content

skip to main content

developerWorks  >  Java technology  >

In pursuit of code quality: Testing Struts legacy apps

Keep Struts apps running smooth with StrutsTestCase and DbUnit

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss


Rate this page

Help us improve this content


Level: Intermediate

Andrew Glover (aglover@stelligent.com), President, Stelligent Incorporated

28 Jul 2006

Even as Struts does a slow fade into the Web Framework Hall of Fame, its legacy lives on, mostly in the form of applications that need to be tested and maintained. This month, Andrew Glover shows you how to put the quality-centered approach to the test (so to speak) on Struts, using JUnit's StrutsTestCase, DbUnit, and some of what you've learned so far in this series.

The landscape for Java™-based Web development has recently blossomed into a cornucopia of competing technologies. Developers launching new projects can pick and choose among a great variety of frameworks, including JavaServer Faces, Tapestry, Shale, Grails, and Seam (to name just a few in the litany of clever sobriquets). Soon it seems we'll even be able to use Ruby on Rails in our Java programming through the JRuby framework!

In the not so distant past, however, there was but one Java Web development framework that stood out among the few. Struts was the first Web development framework to take the Java world by storm, and for many years it seemed that a project that didn't build off of Struts was inherently doomed. A Java developer without Struts experience was as rare and unfortunate then as a developer today who hasn't heard of Ruby on Rails.

Improve your code quality

Don't miss Andrew's accompanying discussion forum for answers to your most pressing questions.

Even as Struts slowly fades from center stage (the original base framework, now called Struts 1, appears headed for the Web Framework Hall of Fame), its legacy remains, both in the form of Shale (see Resources) and in the thousands upon thousands of legacy applications running all across the world. Because many businesses prefer to test and maintain these applications rather than take on the expense of rewriting them, it's a good idea to understand some of the pitfalls of Struts apps, as well as how to refactor your way around them.

This month, I walk you through a quality-centered approach to a Struts application testing scenario. True to life, the scenario revolves around the most ubiquitous of Struts constructs: the much-loved Action class.

1, 2, 3, Action!

One of the revolutions of Struts was that it moved Web development away from Servlets and into Action classes. These classes embodied business logic and ferried data, in the form of a JavaBean commonly called an ActionForm, to JSPs. The JSPs then handled the application view. The Struts approach to MVC is very easy to grasp, so much so that many development teams dove in head first, with little consideration to the long-term design and maintenance issues associated with Action classes.

Testing and complexity

I've found a strong correlation between developer testing and code complexity: where there is one, there usually isn't the other. Code that is highly complex is hard to test, with the result that few people actually write tests for it. Conversely, writing tests as you go along cuts down on the complexity of your code. Because it's harder to write tests for complex code and because you're testing as you go, you'll find yourself gravitating toward simpler code constructs. If your code is too complex and you know you have to test it, you'll probably refactor for complexity before you test. However you look at it, writing tests for non-trivial code is great practice for keeping code complexity at bay.

While no one would have thought it at the time (ah, the freewheeling days of yore), Struts Action classes often become havens for complexity. Like the infamous Session Facades found in older EJB architectures, an Action class can become a rigid facade for a particular business process, either by directly calling EJBs, by opening connections to databases, or by invoking other highly dependent objects. Action classes also have an efferent coupling (via objects in the java.servlet API packages such as HttpServletRequest and HttpServletResponse), making them extremely hard to test in isolation.

The difficulty of testing Action classes in isolation means that they can easily become quite complex -- especially as they become ever more deeply coupled in a legacy framework. Now let's look at how this difficultly plays out in a realistic legacy application scenario.

A testing challenge

Even the simplest Struts Action class can present a testing challenge. For example, take the execute() method in Listing 1; it appears simple enough to test, doesn't it?


Listing 1. This method seems easy to test ...

public ActionForward execute(ActionMapping mapping, ActionForm aForm, 
		HttpServletRequest req, HttpServletResponse res) throws Exception {
 try{
  
   String newPassword = ((ChangePasswordForm)aForm).getNewPassword1();
   String username = ((ChangePasswordForm)aForm).getUsername();

   IUser user = DataAccessUtils.getDaos().getUserDao().findUserByUsername(username);

   user.digestAndSetPassword(newPassword);
   DataAccessUtils.getDaos().getUserDao().saveUser(user);	

 }catch(Throwable thr){				
     return findFailure(mapping, aForm, req, res);
 }
 return findSuccess(mapping, aForm, req, res);	
}


Figure 1. Efferent coupling for the Action class

As you can see in Figure 1, however, the class actually presents some challenges that show themselves if you try to isolate the ChangePasswordAction class and verify its execute() method. To effectively test the execute() method, you have to deal with three layers of coupling. First, there is the coupling to Struts itself; second, the Servlet API presents a hurdle; and finally, there is a coupling to a business object package, which upon further inspection happens to be a data access layer that uses Hibernate and Spring.

A mock for every occasion?

Even as I write this, I can hear the mockery of developers who will claim that my testing problem could be easily solved with a judicious use of mock objects. You can use mock objects to create a level of isolation, which results in easier testing; but, I'd argue that the level of effort required to mock out the desired objects outweighs the effort required to simply concede that testing in isolation is difficult. In this case, I would approach testing at a higher level, which is sometimes called integration testing.

For even more complexity, note how the code in Listing 1 casts the aForm parameter to a ChangePasswordForm object, which is a Struts ActionForm type. These JavaBeans have a validate method, which Struts invokes internally before invoking the execute() method on the Action class.

To err is too easy

In Listing 2, you can see where all this complexity could go awry. A snippet from ChangePasswordForm's validate() method demonstrates simple logic to ensure that two properties (newPassword1 and newPassword2) are not blank and actually equal each other. If Struts discovered that the errors collection (of type ActionErrors) contained some ActionError objects, however, it would then follow an error path, such as redisplaying the Web page with error messages.


Listing 2. Validate logic from ChangePasswordForm

if((newPassword1 == null) || (newPassword1.length() < 1)) {
  errors.add("newPassword1", 
    new ActionError("error.changePassword.newPassword1Required"));
}

if((newPassword2 == null) || (newPassword2.length() < 1)) {
  errors.add("newPassword2", 
    new ActionError("error.changePassword.newPassword2Required"));
}

if((newPassword1 != null) && (newPassword2 != null)) {
  if(!newPassword1.equals(newPassword2)) {
    errors.add(ActionErrors.GLOBAL_ERROR, 
	 new ActionError("error.changePassword.passwordsDontMatch"));
  }
}

The code in Listing 1 and Listing 2 isn't special or specific to a domain. It's simple password-change logic contained in tons of applications. If you're testing Struts legacy apps, you'll likely have to deal with password logic sometime, but how will you test it in a repeatable way?



Back to top


Two test cases

Before attempting to write a test for the code in Listing 1 (and indirectly Listing 2), you might want to ascertain what exactly requires testing. In this particular case, the logic is clearly geared toward facilitating user password changes; therefore, you could write at least two high-level test cases:

  • Does the password change work with correct data?
  • If the data is incorrect, does the password not change?

It's a given that these tests aren't going to be easy. Not only do you have to contend with Struts, you also have to wrestle with the data layer and its implicit coupling to a database! When faced with complexity, my first instinct is to look for help, which in this case comes in the form of JUnit's StrutsTestCase.



Back to top


Help from StrutsTestCase

StrutsTestCase is a JUnit extension that specifically targets Struts applications. This framework essentially mocks a servlet container, such that you can run and test a Struts application virtually rather than having to run it in Tomcat (for instance). The framework also has a handy MockStrutsTestCase class, which extendsTestCase and handles a lot of the Struts configuration aspects for you (such as loading the struts-config.xml configuration file).

Before you think you're completely free of Struts configuration woes, however, you should know a few things about properly configuring MockStrutsTestCase. Namely, you need to point it to the directory that represents your Web application and then point it to the necessary web.xml and struts-config.xml files. By default, MockStrutsTestCase scans a classpath for these items; however, configuring MockStrutsTestCase to work in a specific environment is as simple as overriding setup and writing some specific configuration code.

Returning to the password-verification example, the project that contains the ChangePasswordAction class has the directory structure shown in Listing 3:


Listing 3. Example directory listing

root/
  src/
    conf/
    java/
    webapp/
      images/
      jsp/
      WEB-INF/
  test/

The WEB-INF directory contains both the web.xml and struts-config.xml files and the webapp directory represents the Web context. Knowing this, I configured MockStrutsTestCase as shown in Listing 4:


Listing 4. Custom fixture code for MockStrutsTestCase

public void setUp() throws Exception {
 try {
  super.setUp();

  this.setContextDirectory(new File("src/webapp/"));
  this.setServletConfigFile("src/webapp/WEB-INF/web.xml");
  this.setConfigFile(
       this.getSession().getServletContext()
	      .getRealPath("WEB-INF/struts-config.xml"));
  
  }catch (Exception e) {
   fail("Unable to setup test");
  }
}

Other ways to test

In some cases, based on the corresponding logic found in an Action class, you might be able to indirectly test code using a Web-based testing framework like JWebUnit or Selenium. Using these frameworks does add complexity in terms of test set up, however. To use JWebUnit, for example, you have to deploy the application to a servlet container with a configured database running. Using StrutsTestCase in concert with DbUnit facilitates testing without having to deploy a war file to a running servlet container. It also allows you to test without having to consider the view aspect of an application.

About logical mapping

Having properly configured an instance of MockStrutsTestCase, testing Action classes just involves a bit of logical mapping. To invoke an Action class, you need to force the StrutsTestCase framework to indirectly call it through a path, which is defined in the struts-config.xml file.

For example, to force an invocation of the ChangePasswordAction class, you must tell the framework to use the /changePasswordSubmit path. You can see this in Listing 5, a snippet from the struts-config.xml file that maps the ChangePasswordAction class to the /changePasswordSubmit path:


Listing 5. struts-config.xml snippet showing action class path mapping

<action path="/changePasswordSubmit"
         type="com.acme.ccb.action.ChangePasswordAction" 
         name="changePasswordForm" scope="request" 
         input="/jsp/admin/changepassword.jsp">
    
  <forward name="success" path="/viewUsers.do" 
           redirect="true" contextRelative="false" />

</action> 

Once a user hits the submit button (for example), Struts maps parameter values from the HTTP request into ActionForms, which in this case, is the ChangePasswordForm as defined in the struts-config.xml snippet above (in Listing 5). To mimic this behavior, another logical mapping must take place in a test case -- JSP form names must be mapped to values. In the password change scenario, four parameters are submitted: username, currentPassword, newPassword1, and lastly, newPassword2 ( the newPassword2 parameter is the confirmation most Web pages have to verify a correct new password).



Back to top


A successful test case!

With the request path and parameters mapped, writing a test case becomes a matter of utilizing the MockStrutsTestCase API to set associated values, as shown in Listing 6. In this test case, user Jane's password is changed from "admin" to "meme."


Listing 6. A simple test case verifying a successful password change

public void testExecute() throws Exception{		
 setRequestPathInfo("/changePasswordSubmit");		

 addRequestParameter("username","jane");
 addRequestParameter("currentPassword","admin");
 addRequestParameter("newPassword1","meme");
 addRequestParameter("newPassword2","meme");

 actionPerform();		
 verifyForward("success");			
}

The setRequestPathInfo() method configures the path to map to an Action class and the addRequestParameter() methods map parameter names from the JSP file to values. For example, in Listing 6, the username parameter is mapped to "jane."

Also note the last two lines of code in Listing 6. The actionPerform() method actually spurs Struts to invoke the corresponding Action class. If this method isn't called, nothing happens. The last method invoked, verifyForward(), is an assertion-like method found in the MockStrutsTestCase class that verifies the proper forward. In Struts, this is a String usually mapped to a success or a failure state. (Note the XML in Listing 5, which defines the "success" forward.)



Back to top


Repeatable success with DbUnit

At this point, you might expect that your work is done -- you've written a test that attempts to verify a password change, after all. But deeper verification is lacking. Sure, this handy framework fires up Struts, but the code relies on a database. If you want to be able to run this test more than once, say in a build process, you need to make it repeatable.

The test case in Listing 6 isn't repeatable because of some specific assumptions. First, the test case assumes there is a user named "jane" who has the password "admin" already in the system. Second, the test case assumes that the password "admin" is updated to "meme" in some permanent store. As written, so long as no exception is generated by the code and the ActionForm successfully validates, Struts assumes things worked just fine, and so will the test case.

What you need is a deeper level of validation -- at the database level. For test cases where the password should be updated, you should ideally perform a check on the database to ensure a new password is there. For tests where a password shouldn't change, verification is required to truly verify an unchanged password. Last, to make this test suite repeatable, it would be nice to not make any assumptions regarding data integrity.

DbUnit is a JUnit extension that facilitates placing a database into a known state between test runs. Using XML seed files, you can insert specific data into a database, which a test case can then rely on. Moreover, using the DbUnit API, you can easily compare the contents of the database to the contents of an XML file, thus providing a mechanism to verify expected database results outside of application code.



Back to top


Testing with DbUnit

To use DbUnit, you need two things:

  • A database connection via normal JDBC
  • A file containing the data needed to seed the database

Listing 7 is a DbUnit seed file that defines a few things: first, there is a table called user and another called user_role. You define a new row in the user table and map some values to columns (such as column username having the value "jane"). You also define a row in the user_role. Note that passwords in this database are encrypted via SHA.


Listing 7. DbUnit seed file for testing tables user and user_role

<?xml version='1.0' encoding='WINDOWS-1252'?>
<dataset>
 <!-- user with password admin -->
 <user username="jane" 
   password="d033e22ae348aeb5660fc2140aec35850c4da997" 
   name="Jane Admin"
   date_created="2003-8-14 10:10:10"
   email="jane@elsewhere.org"/>
 
 <user_role username="jane" rolename="ADMIN"/>
</dataset>

With this file, you could employ DbUnit to insert the data, update the database to reflect the data, and even delete the data. Database modification logic is embodied in DbUnit's DatabaseOperation class. In this case, you simply ensure a clean data set via the CLEAN_INSERT flag in some enhanced fixture logic in the setUp() method of the MockStrutsTestCase type defined in Listing 4. For instance, in Listing 8, you define three methods that utilize the DbUnit API to insert the contents of the dbunit-user-seed.xml file into a database.


Listing 8. Custom DbUnit fixture logic

private void executeSetUpOperation() throws Exception{		
 final IDatabaseConnection connection = this.getConnection();
 try{
  DatabaseOperation.CLEAN_INSERT.execute(connection, this.getDataSet());
 }finally{
  connection.close();
 }
}			

private IDataSet getDataSet() throws IOException, DataSetException {
 return new FlatXmlDataSet(new File("test/conf/dbunit-user-seed.xml"));
}

private IDatabaseConnection getConnection() throws ClassNotFoundException, SQLException {
 final Class driverClass = Class.forName("org.gjt.mm.mysql.Driver");
 final Connection jdbcConnection = DriverManager.
   getConnection("jdbc:mysql://localhost/ccb01", 
     "9043", "43xli");               
 return new DatabaseConnection(jdbcConnection);
}

The executeSetUpOperation() method defined in Listing 8 will be invoked in the setUp() method previously defined in Listing 4. This method, in turn, invokes the remaining two methods in Listing 8: getDataSet(), which converts the XML file into DbUnit's IDataSet type, and getConnection(), which returns a database connection wrapped as DbUnit's IDatabaseConnection type.



Back to top


An even better test case

With DbUnit configured, all that's left to do is enhance the test case from Listing 6 to verify that everything is okay in the database. Then, you add the remaining test cases that verify your non-sunny-day scenarios.

To confirm an updated password in the database, you use DbUnit's Query API, which facilitates comparing database results with statically defined XML files, such as the one defined in Listing 9. Note how this file doesn't list all the columns in the user table -- in fact, it only lists two: username and password.


Listing 9. Compare to test XML file

<?xml version='1.0' encoding='WINDOWS-1252'?>
<dataset>
 <user username="jane" 
    password="58117e24e4d0b8a958146c9eaa28336184f4d491"/>
 
</dataset>

DbUnit's Query API is flexible enough to facilitate the filtering of non-essential values, which in this case is everything but the username and password. As such, in Listing 10, the verifyPassword() method uses DbUnit's createQueryTable() method to build a ITable type to compare with the XML defined in Listing 9:


Listing 10. verifyPassword method using DbUnit's Query API

private void verifyPassword(String fileName) throws Exception{
 final IDataSet expectedDataSet = new FlatXmlDataSet(
   new File(fileName));

 final ITable defJoinData = this.getConnection().
  createQueryTable("TestResult",
    "select user.username, user.password " +
    "from user where user.username=\"jane\"");

 final ITable defTable = expectedDataSet.getTable("user");
 Assertion.assertEquals(defJoinData, defTable);
}

The Assertion type is a custom class defined by DbUnit to facilitate additional assertions specific to database result sets. Note too, the verifyPassword() takes a path to a file, meaning I can define multiple expected data sets (one for a changed password and another for the same one).



Back to top


Test Struts again and again

Plugging everything together, you now have a test case that can do the following:

  1. Seed the database via DbUnit
  2. Configure Struts
  3. Indirectly invoke the ChangePasswordAction and ChangePasswordForm classes
  4. Associate parameter values
  5. Verify a successful forward
  6. Verify database contents

As you see in Listing 11, the ChangePasswordAction test case only handles one sunny-day scenario through the testExecute test:


Listing 11. ChangePasswordAction test case

package test.com.acme.ccb.action;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import org.dbunit.Assertion;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;

import servletunit.struts.MockStrutsTestCase;

public class ChangePasswordActionTest extends MockStrutsTestCase {

 public ChangePasswordActionTest(String arg0) {
  super(arg0);		
 }

 public void setUp() throws Exception {
  try {
   super.setUp();	

   this.executeSetUpOperation();

   this.setContextDirectory(new File("src/webapp/"));
   this.setServletConfigFile("src/webapp/WEB-INF/web.xml");
   this.setConfigFile(
     this.getSession().getServletContext()
        .getRealPath("WEB-INF/struts-config.xml"));
  } catch (Exception e) {
    fail("Unable to setup test");
  }
}

 public void testExecute() throws Exception{		
   setRequestPathInfo("/changePasswordSubmit");		

   addRequestParameter("username","jane");
   addRequestParameter("currentPassword","admin");
   addRequestParameter("newPassword1","meme");
   addRequestParameter("newPassword2","meme");

   actionPerform();		
   verifyForward("success");	

   verifyPassword("test/conf/dbunit-expect-user.xml");
 }

 private void executeSetUpOperation() throws Exception{		
  final IDatabaseConnection connection = this.getConnection();
  try{
    DatabaseOperation.CLEAN_INSERT.execute(connection, this.getDataSet());
  }finally{
    connection.close();
  }
 }			

 private IDataSet getDataSet() throws IOException, DataSetException {
  return new FlatXmlDataSet(new File("test/conf/dbunit-user-seed.xml"));
 }

 private IDatabaseConnection getConnection() throws ClassNotFoundException, SQLException {
  final Class driverClass = Class.forName("org.gjt.mm.mysql.Driver");
  final Connection jdbcConnection = DriverManager.
   getConnection("jdbc:mysql://localhost/ccb01", 
     "9043", "43xli");               
  return new DatabaseConnection(jdbcConnection);
 }

 private void verifyPassword(String fileName) throws Exception{
  final IDataSet expectedDataSet = new FlatXmlDataSet(
   new File(fileName));

  final ITable defJoinData = this.getConnection().
   createQueryTable("TestResult",
    "select user.username, user.password " +
    "from user where user.username=\"jane\"");

  final ITable defTable = expectedDataSet.getTable("user");
  Assertion.assertEquals(defJoinData, defTable);
 }
}

Just one more test ...

Note that this test case doesn't test edge cases, such as if the two password fields (newPassword1 and newPassword2()) didn't match. Thankfully, it isn't hard to add another test case once you've set things up. In Listing 12, you verify that if the two values don't match, an ActionError is generated and that the database value of password for user "jane" remains unchanged.


Listing 12. Adding a new test

public void testExecuteWithErrors() throws Exception{		
   setRequestPathInfo("/changePasswordSubmit");		

   addRequestParameter("username","jane");
   addRequestParameter("currentPassword","admin");
   addRequestParameter("newPassword1","meme");
   addRequestParameter("newPassword2","emem");

   actionPerform();		
   verifyActionErrors(
     new String[]{"error.changePassword.passwordsDontMatch"});
   verifyPassword("test/conf/dbunit-expect-user-same.xml");
 }

In Listing 12, I verified that the logic in Listing 2 correctly caught that the password values don't match. The MockStrutsTestCase class contains a handy method for asserting error conditions, verifyActionErrors(), in which the error String is passed in for verification. Note too, the database is examined, this time against a different file that contains the same values (in this case, username "jane" with an unencrypted password of "admin").



Back to top


Integration testing for Struts

Most Struts applications are not going away anytime soon, so it's important to know how to build in a level of assurance with developer tests before embarking on a rewrite. This month, I've introduced some of the challenges in testing Struts legacy applications and shown you how to work around them using StrutsTestCase and DbUnit.

StrutsTestCase handles the plumbing of Struts as long as it's properly configured and DbUnit handles the plumbing of database-related code. Using these two frameworks together lets you do integration-level testing on Struts apps rather than mimicking a browser through a higher level framework such as JWebUnit or Selenium. (Also a worthwhile approach, but the resulting tests are very different.)

Struts applications are challenging to test; there's no way around it. This difficulty is one of the reasons the Struts framework has been overshadowed by more innovative frameworks, especially those that specifically address ease of testing. On the other hand, as I've showed you here, testing Struts is possible -- it just takes some elbow grease.



Resources

Learn

Get products and technologies
  • StrutsTestCase: An extension of the standard JUnit TestCase class that provides facilities for testing code based on the Struts framework.

  • DbUnit: A JUnit extension (usable with Ant) that puts a database into a known state between test runs.

  • Shale: The next generation of Struts.

  • Selenium: A test tool for Web applications that runs tests directly in a browser, just as real users do.


Discuss


About the author

Andrew Glover is the President of Stelligent Incorporated, which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. He is the co-author of Java Testing Patterns (Wiley, September 2004).




Rate this page


Please take a moment to complete this form to help us better serve you.



YesNoDon't know
 


 


12345
Not
useful
Extremely
useful
 


Back to top