///StrutsTestCase Simplifies the Development Process

StrutsTestCase Simplifies the Development Process

Save Time by Using STC’s Mock and Cactus Testing Approaches

This article introduces the StrutsTestCase (STC) framework and explains how to test a sample application using the mock approach and Cactus approach. Author Sunil Patil, a developer at the IBM Software Labs in India, introduces STC, then walks you through setting up an environment for using STC and testing various Struts features. He also demonstrates using both the Cactus and mock approaches from within STC.

Note: This article assumes familiarity with the Struts framework.

The StrutsTestCase (STC) framework is an open source framework for testing Struts-based Web applications. This framework allows you to test things like:

• Validation logic in your ActionForm class (the validate() method)

• Business logic in your Action class (the execute() method)

• Action Forwards

• Forward JSPs

STC supports two types of testing:

• Mock Approach — In this approach, STC allows you to test your application without deploying it in an application server by mocking the container-provided object (HttpServletRequest, HttpServletResponse, and ServletContext).

• Cactus Approach — In this approach, used during the integration testing phase, the application will be deployed in a container, and you will be able to run your test case like any other JUnit test case.

Sample Application

I’ll start by walking you through the creation of a sample Struts application, which will be the basis for our testing. You can use struts-blank.war, which is shipped with Struts, or your favorite IDE to create it. The sample application will have a login page on which a user will enter his user name and password. If the login is successful, the user will be redirected to a success page. If the login fails, he will be redirected to the login page.

The source code that accompanies this article is available by selecting the Code icon at the top or bottom of this article.

The Login.jsp page

Create the login page, as shown in Listing 1:

Listing 1. Login.jsp

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html>
<HEAD>
<%@ page language="java"contentType="text/html;
charset=ISO-8859-1"pageEncoding="ISO-8859-1" %>
<META http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<TITLE>Login.jsp</TITLE>
</HEAD>
<BODY>
<html:form action="/login">
<html:errors/>
<H3>Login</H3>
<TABLE border="0">
<TBODY>
<TR>
<TH>User Name</TH>
<TD><html:text property='userName' value='' /></TD>
<TR>
<TR>
<TH>Password</TH>
<TD><html:text property='password' value='' /></TD>
</TR>
<TR>
<TD><html:submit property="submit" value="Submit" /></TD>
<TD><html:reset /></TD>
</TR>
</TBODY>
</TABLE>
</html:form>
</BODY>
</html:html>

The LoginActionForm.java class

Create the LoginActionForm.java class, as shown in Listing 2:

Listing 2. LoginActionForm.java

public class LoginActionForm extends ActionForm {

public ActionErrors validate(
ActionMapping mapping,
HttpServletRequest request) {

ActionErrors errors = new ActionErrors();
if (userName == null || userName.length() == 0)
errors.add("userName", new ActionError("username.required"));
if (password == null || password.length() == 0)
errors.add("password", new ActionError("password.required"));

if( isUserDisabled(userName))
errors.add("userName",new ActionError("user.disabled"));
return errors;
}

//Query USERDISABLED table to check if user account is disabled
public boolean isUserDisabled(String userName) {
//SQL logic to check if user account is disabled
}
}

In the validate() method, you want to check that the user enters a user name and password because those fields are mandatory. Also, you want to query the USERDISABLED table to verify that the user account is not disabled.

The LoginAction.java class

Next, create the LoginAction.java class, as shown in Listing 3:

Listing 3. The LoginAction.java class

public class LoginAction extends Action {

public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (isValidUser(loginForm.getUserName(), loginForm.getPassword())) {
request.getSession().setAttribute(
"userName",
loginForm.getUserName());
return mapping.findForward("success");
} else {
ActionErrors errors = new ActionErrors();
errors.add("userName", new ActionError("invalid.login"));
saveErrors(request, errors);
return new ActionForward(mapping.getInput());
}
}
//Query User Table to find out if userName and password combination is right.
public boolean isValidUser(String userName, String password) {
//SQL Logic to check if username password combination is right
}
}

Here, the execute() method verifies that the user name and password are valid. The sample application uses the USER table to store the user name and password. If the user’s credentials are valid, the user name is saved in the request scope, and he is forwarded the success page (Success.jsp).

The struts-config.xml file

Create the struts-config.xml file, as shown in Listing 4:

Listing 4. The struts-config.xml file

<action-mappings>

<action path="/login" type="com.sample.login.LoginAction"
name="loginForm" scope="request" input="Login.jsp">
<forward name="success" path="/Success.jsp"/>
</action>
</action-mappings>

If the login is unsuccessful, we redirect the user to the login page.

The Success.jsp page

Create the Success.jsp page, as shown in Listing 5:

Listing 5. The Success.jsp page

<HTML>

<HEAD>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ page language="java" contentType="text/html; %>
<META http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<TITLE>Success.jsp</TITLE>
</HEAD>
<BODY>
<%
String userName = (String)session.getAttribute("userName");
%>
Login Successful<br/>
<P>Welcome: <%=userName%> .</P>
</BODY>
<HTML>

Here, the userName attribute is read from the request scope and is used to greet users who have logged in.

Using the Mock Object Approach

Mock testing is a popular approach for unit testing applications. Refer to Resources if you’re new to the mock testing approach and want to know more.

Setup for the mock approach

To use the mock approach, we will have to make a few changes to the sample application. We will start by writing a mock test:

1. Add strutstest-2.1.•.jar and junit3.8.1.jar to the classpath

2. Add the WEB-INF folder to your classpath

3. Create a MockLoginTestAction class that extends the MockStrutsTestCase class

4. Run the unit test case

Now that you have set up the environment, you can start writing the unit test cases.

Blank user name or password

First, you want to verify that if the user leaves the user name or password fields blank, he is shown an appropriate error message and redirected to the login page. Create the testLoginActionFormError() method in the MockLoginTestAction class, as shown in Listing 6:

Listing 6. The testLoginActionFormError() method

public void testLoginActionFormError()throws Exception{

setRequestPathInfo("/login");
actionPerform();
String[] actionErrors = {"username.required","password.required"};
verifyActionErrors(actionErrors);
verifyInputForward();
}

The first thing you need to do when writing the STC test case is to tell STC which ActionMapping class you want to test. In the sample, we want to test LoginAction, which is mapped to the “/login” path in struts-config.xml, so we will have to call setRequestPathInfo(“/login”). By default, STC looks in the /WEB-INF/ folder for struts-config.xml. If this file is not in the classpath, you can call setConfigFile() with the complete path of your struts-config.xml file.

Now you’re ready to execute the test case. A call to the actionPerform() method executes the test case by passing control to the Struts framework. Once control has returned from actionPeform(), you can test the assumptions by calling the verifyXXX() method. In the sample application, we want to test whether the LoginAction mapping, when called without a user name and password, redirects the user to the login page with ActionErrors for username.required and password.required. The verifyInputForward() method checks that, as a result of this transaction, the user is redirected to the page indicated by the input attribute in our action mapping, which in this case is Login.jsp.

You can call verifyActionErrors() with the array of Strings indicating which ActionErrors should be set in request scope as a result of this transaction. We are expecting that username.required, password.required, and ActionErrors would be set, so we create an array of Strings with these errors and pass them to the verifyActionErrors() method.

How the STC mock approach works

ActionServlet is a controller servlet in the Struts framework. When a container gets a request, it passes the request to ActionServlet, which does all the request processing.

The basic idea behind STC is to create the object of ActionServlet yourself, instead of allowing the container to create it, then calling its appropriate methods. ActionServlet requires ServletContext and ServletConfig objects at the time of initialization, and requires objects of HttpServletRequest and HttpServletResponse at the time of request processing. STC creates mock objects of these classes and passes them on to Struts.

MockStrutsTestCase is a JUnit test case that extends the junit.framework.TestCase class, so the setup() method is executed for every test case. In the setup() method of MockStrutsTestCase, STC creates objects for ActionServlet and other required mock objects.

When you call the setRequestPathInfo() or addRequestParameter() methods, the appropriate methods of a mock HttpServletRequest object are called. In the mock implementation of HttpServletRequest, it stores this information to the appropriate setup state. So if you call addRequestParameter(“name”,”value”), the mock HttpServletRequest object stores it, and when Struts calls request.getParameter(“name”), it returns “value” as the value.

Once you are done initializing HttpServletRequest properly, you can pass control to Struts by calling the actionPerform() method. The actionPerform() method calls the doPost() method on ActionServlet, passing the mock implementation of HttpServletRequest and HttpServletResponse.

In the doPost() method of ActionServlet, the request is processed like any other Struts request, except that request processing is stopped just before the ActionForward JSP component is executed. In this stage, the state of the mock objects is changed to note things like what ActionErrors or ActionMessages were saved, or what was the resulting ActionForward.

Once control returns from the actionPerform() method, you can check our various assumptions by calling the appropriate verifyXXX() method, which checks the state of mock objects.

Test disabled user

There is one problem with the isUserDisabled() method of the LoginActionForm class. In this method, we query the USERDISABLED table to find out whether the user account is disabled. In the current context, we do not want to waste time setting up and querying the database.

Keep in mind that our goal is to check the Struts part of the application, not the database interaction code. To test the database interaction code, you could choose from several available tools, such as DBUnit. The best approach for this situation would be to create a subclass of the LoginActionForm class and override the isUserDisabled() method in it. This method should return true or false based on the input parameter values.

Say in this case, the method will always return true unless you call it with disabledUser as an input parameter. Now this method should be used only during the unit testing phase, and the main LoginActionForm should not know about it. For this type of requirement, I created STCRequestProcessor, which extends RequestProcessor. It allows you to insert a mock implementation of your Action and ActionForm class.

To use STCRequestProcessor, change struts-config.xml, as shown in Listing 7:

Listing 7. The struts-config.xml file

<controller>

<set-property property="processorClass" value="com.sample.util.STCRequestProcessor"/>
</controller>
</code>

This line instructs Struts to use STCRequestProcessor.java as RequestProcessor. Don’t forget to remove these lines when you deploy your application in a container.

Next up is to create a mock class for LoginActionForm, as shown in Listing 8:

Listing 8. The MockLoginActionForm.java class

public class MockLoginActionForm extends LoginActionForm {

public boolean isUserDisabled(String userName) {
if (userName != null && userName.equals("disableduser"))
return true;
return false;
}
}

The isUserDisabled() method checks if the user name is “disableduser”. If so, only true should be returned; otherwise, we get false.

Next, create a test case to test for a disabled user, as shown in Listing 9:

Listing 9. The testDisabledUser() method

public void testDisabledUser()throws Exception{

STCRequestProcessor.addMockActionForm("loginForm",
"com.sample.login.mock.MockLoginActionForm");
setRequestPathInfo("/login");
addRequestParameter("userName","disableduser");
addRequestParameter("password","wrongpassword");
actionPerform();
verifyInputForward();
String[] userDisabled ={"user.disabled"};
verifyActionErrors(userDisabled);
}

The STCRequestProcessor.addMockActionForm() method inserts MockLoginActionForm as a mock implementation for LoginActionForm. The addRequestParameter() method sets the user name and password request parameters. Once control returns from actionPerform(), we verify that the user is redirected to the input page with the user.disabled error message by calling verifyActionErrors().

Testing the invalid login

Test cases test the business logic inside the execute() method of the LoginAction class. The execute() method calls the isValidUser() method of the same class, which in turn queries the USER table to find out if the user name and password combination is valid. Now, because we do not want to query the real database during the unit testing phase, we will create a mock subclass of the LoginAction class, overriding the isValidUser() method, as shown in Listing 10:

Listing 10. The MockLoginAction.java class

public class MockLoginAction extends LoginAction {

public boolean isValidUser(String userName, String password) {
if( userName.equals("ibmuser") && password.equals("ibmpassword"))
return true;
return false;
}
}

The MockLoginAction class’ isValidUser() method returns true if the user name is “ibmuser” and the password is “ibmpassword”. Call the STCRequestProcessor.addMockAction() method to insert MockLoginAction in place of LoginAction, as shown in Listing 11:

Listing 11. The testInvalidLogin() method

public void testInvalidLogin()throws Exception{

STCRequestProcessor.addMockActionForm("loginForm",
"com.sample.login.mock.MockLoginActionForm");
STCRequestProcessor.addMockAction("com.sample.login.LoginAction",
"com.sample.login.mock.MockLoginAction");
setRequestPathInfo("/login");
addRequestParameter("userName","ibmuser");
addRequestParameter("password","wrongpassword");
actionPerform();
String[] invalidLogin ={"invalid.login"};
verifyActionErrors(invalidLogin);
verifyInputForward();
}

In this test case, mock implementations of LoginAction and LoginActionForm are inserted to avoid database queries, followed by setting the user name and password parameters. After control returns from actionPerform(), we check whether the user is redirected to the login page with “invalid.login” as an error message.

Testing the valid login

It’s time to verify that when the user enters the correct user name and password, he is greeted with the success page, as shown in Listing 12:

Listing 12. testLoginActionFormError

public void testValidLogin() throws Exception{

STCRequestProcessor.addMockActionForm("loginForm",
"com.sample.login.mock.MockLoginActionForm");
STCRequestProcessor.addMockAction("com.sample.login.LoginAction",
"com.sample.login.mock.MockLoginAction");
setRequestPathInfo("/login");
addRequestParameter("userName","ibmuser");
addRequestParameter("password","ibmpassword");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
}

This segment first sets the user name as “ibmuser” and the password as “ibmpassword” in the request parameter, then calls actionPerform(). Once the actionPerform() method is executed, it checks that the user is redirected to the success page by calling the verifyForward() method. Also call the verifyNoActionErrors() method to verify that no ActionErrors occurred during this transaction.

Advantages and Disadvantages of Mocking

There are several advantages to using the mock approach. This approach is fast because you don’t have to start and stop containers for every change. It also allows granular tests. On the other hand, because you don’t use real a container, you can’t verify any side effects introduced by listeners or filters. Also, because ActionForward JSP components are not executed, you won’t be able to find errors in the JSPs.

Cactus Approach

Cactus (in-container) is a popular approach for testing during the integration testing phase. I won’t cover Cactus in detail; see Resources for more information.

Setup for Cactus approach

To set up Cactus, copy cactus.1.6.1.jar and aspectjrt1.1.1.jar into your classpath.

Cactus needs two servlets to be configured in your Web application, so you must declare them in the web.xml file, as shown in Listing 13:

Listing 13. web.xml

<servlet>

<servlet-name<ServletTestRedirector</servlet-name>
<display-name<ServletTestRedirector</display-name>

<servlet-class<org.apache.cactus.server.ServletTestRedirector</servlet-class>
</servlet>

<servlet>
<servlet-name<ServletTestRunner</servlet-name>
<display-name<ServletTestRunner</display-name>

<servlet-class<org.apache.cactus.server.runner.ServletTestRunner</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name<ServletTestRedirector</servlet-name>
<url-pattern</ServletRedirector</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name<ServletTestRunner</servlet-name>
<url-pattern</ServletTestRunner</url-pattern>
</servlet-mapping>

Next, create a cactus.properties file like the one shown below and put it in the classpath:

cactus.contextURL = http://localhost:9080/sample1

cactus.servletRedirectorName = ServletRedirector

I am using the WebSphere® Studio built-in test environment to run my test cases, so my application will be available on http://localhost:9080/sample1. Be sure to change this path to indicate the location where your Web application is deployed.

Next, create a class to extend CactusStrutsTestCase. Because the same test cases can be used in the mock and Cactus approaches, copy the content of MockLoginActionTest in this class. Build and deploy this application in the container of your choice.

Finally, configure jdbc/ds1 as your data source.

How the STC Cactus approach works

When you are using Cactus for testing an application, you will have to deploy that application in a Web container and run another instance of the Cactus test class as any JUnit test case outside container. When you run the Cactus unit test, it creates and executes an HTTP request for the URL indicated by the cactus.contextURL parameter in your cactus.properties file for every test case method in your class.

In the case of the sample application for executing testDisableUser, it will create and execute a request like this:

http://localhost:9080/sample1/ServletRedirector?Cactus_TestMethod=testDisabledUser&Cactus_TestClass=

com.sample.test.CactusLoginActionTest&Cactus_AutomaticSession=true&Cactus_Service=CALL_TEST

This request will invoke the ServletTestRedirector servlet deployed as a part of the sample Web application. In ServletTestRedirector, Cactus looks at the name of the test case class from the Cactus_TestClass request parameter and calls its method indicated by the Cactus_TestMethod parameter. Once that method is executed, it returns results as an HTTP response to the Cactus test class, which is executing an outside container.

In addition, when the in-container version of CactusStrutsTestCase gets control (which is CactusLoginActionTest in our case) in the testDisabledUser() method, STC calls the actionPerform() method, which creates an instance of the ActionServlet, ServletContext, and ServletConfig objects. It also wraps the current request and response in a wrapper. Then it calls the doPost() method of ActionServlet, with these wrapped ServletRequest and ServletResponse objects. Struts then processes the request as usual.

With the Cactus approach, you can tell STC to verify forward JSPs by calling the processRequest(true) method, which results in the forward JSP being executed and tested to ensure that it does not throw any compile and run-time errors.

Once control returns from actionPerform(), you can call various verifyXXX() methods to check your assumptions.

Test forward JSP errors

Test to make sure that Success.jsp has no compile time or run-time errors by changing the testVaidLogin() method, as shown in Listing 14:

Listing 14. The testValidLogin() method

public void testValidLogin() throws Exception{


STCRequestProcessor.addMockActionForm("loginForm","com.sample.login.mock.MockLoginActionForm");

STCRequestProcessor.addMockAction("com.sample.login.LoginAction","com.sample.login.mock.MockLoginAction");
processRequest(true);
setRequestPathInfo("/login");
addRequestParameter("userName","ibmuser");
addRequestParameter("password","ibmpassword");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
}

Also change Success.jsp to make it throw a RunTimeException by adding the following lines:

<%

throw new RuntimeException("test error");
%>

Now when you run this test case, testValidLogin() will create and execute database queries to check that the user account is not disabled and that the user name and password are valid. The test will then fail, indicating that it got a run-time error while executing Success.jsp.

Cactus Pros and Cons

Using Cactus can certainly be an advantage, but it’s not without its difficulties. On the plus side, it lets you test your forward JSPs for compile and run-time errors, and lets you test your data access code. On the downside, this approach requires deploying our application in a container, then starting and stopping it for every change, making it a slower approach than the mock approach.

Conclusion

Unit testing offers many advantages. In addition to giving you confidence that your code is working as designed, testing is a source of great documentation. Further, unit tests provide an excellent mechanism for feedback as you design the classes and interfaces. Finally, unit tests are helpful for managing change. If your code passes all unit test cases after you’ve implemented changes, you can feel confident that those changes are safe.

Unfortunately, many developers forgo unit tests because they take too much time to write. But by using STC’s mock approach, you can save a lot of time that you would normally spend setting up the development environment for domain objects like databases and containers. STC also helps you test your changes rapidly because you don’t have to start and stop the container every time. And once your code is stable and able to pass all test cases, you can move to integration testing just by changing the super class of your test case. Using Cactus in the integration phase also allows you to automate your integration tests.

Resources

Struts, an open source MVC implementation (developerWorks, February 2001) by Malcolm Davis provides a good introduction to Struts.

Writing a simple Struts application using WebSphere Studio V5 (developerWorks, February 2003) by Colin Yu and Jane Fung shows how to create a simple Struts application using built-in support in WebSphere Studio V5.0.

• ” Developing and Unit Testing with Open Source Apache Cactus Framework Tools in WebSphere Studio Application Developer” (WebSphere Developer Technical Journal, June 2002) by Sheldon Wosnick shows you how to set up the Cactus framework with WebSphere Studio.

Unit testing with mock objects (developerWorks, November 2002) by Alexander Day Chaffee and William Pietri provides valuable information about mock testing.

• Find hundreds of Java technology resources on the developerWorks Java™ technology zone.

• Visit the Developer Bookstore for a comprehensive listing of technical books, including hundreds of Java technology-related titles.

2010-05-26T11:26:14+00:00 April 28th, 2005|Java|0 Comments

About the Author:

Sunil Patil is a software engineer at IBM India in the Lotus division. He has almost five years of J2EE experience.

Leave A Comment