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.
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.
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.
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.
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.
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.
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?
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.
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");
}
}
|
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).
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.)
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.
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.
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).
Plugging everything together, you now have a test case that can do the following:
- Seed the database via DbUnit
- Configure Struts
- Indirectly invoke the
ChangePasswordActionandChangePasswordFormclasses - Associate parameter values
- Verify a successful forward
- 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);
}
}
|
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").
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.
Learn
- "Effective Unit Testing with DbUnit" (Andrew Glover, OnJava, January 2004): Andrew Glover introduces DbUnit.
- "Control your test environment with DbUnit and Anthill" (Philippe Girolami, developerWorks, April 2004): Learn how to use DbUnit in conjunction with JUnit to control the test environment.
- "In pursuit of code quality: Code quality for software architects" (Andrew Glover, developerWorks, April 2006): Use coupling metrics to support your
system architecture.
- "Testing legacy code" (Elliotte Rusty Harold, developerWorks, April 2006): What to do when you stumble across legacy code that's never been tested.
- "Hip system tests with Cargo" (Andrew Glover, thediscoblog.com, April 2006): Introduces a Web container management framework that facilitates system testing.
- The Java technology zone: Hundreds of articles about every aspect of Java programming.
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
- Participate in the discussion forum.
- developerWorks
blogs: Get involved in the developerWorks community!

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).
Comments (Undergoing maintenance)





