Recipe13.6.Testing Your Actions with Mock Objects


Recipe 13.6. Testing Your Actions with Mock Objects

Problem

You want to unit test your Struts actions without running the application on an application server.

Solution

Use the StrutsTestCase framework to create a unit test, extending the StrutsTestCase MockStrutsTestCase base class, which verifies that your action does what it's supposed to do.

Discussion

The StrutsTestCase framework (http://strutstestcase.sourceforge.net), a JUnit extension, specifically targets the testing of Struts actions. You can download StrutsTestCase from http://strutstestcase.sourceforge.net. If you are coding to the Servlet 2.2 specification, download the binary for 2.2, strutstest212_1.1-2.2.zip; for Servlet 2.3 or later, download strutstest212_1.1-2.3.zip. You should also download the source code for StrutsTestCase; when you need to use a debugger, you can step into the StrutsTestCase code. Unzip the binary and source zip files into a directory.

Copy the strutstest-2.1.2.jar to a location on your application's classpath. Separate your test source code, classes, and libraries from the rest of your application code. You should create a test directory for the application that contains the following:

  • A src folder your test source code

  • A classes folder to compile the test source into

  • A lib folder containing test-specific JAR files such as the StrutsTestCase Jar

When testing, place the test/classes directory and the JAR files in your test/lib directory on your classpath. When you build the actual distribution for your application (for example, the WAR file), you can easily exclude the test directory. Without a separate test directory, it's much more cumbersome to separate test code from production code.

StrutsTestCase uses your application's struts-config.xml file and web.xml file for performing the test, as well as verifying results. So Struts-TestCase can find these files, you must place the directory that contains your WEB-INF directory on your classpath.


With StrutsTestCase, you can create tests that run standalone outside of the servlet container. StrutsTestCase simulates the servlet container using mock objects that represent the servlet-related managed objects such as the HttpServletRequest, HttpServletResponse, HttpSession, and ServletContext. This approach runs tests easier and faster and allows you to make your test immune to side effects of the servlet container and other external objects. Your test focuses exclusively on the Action being tested.

The Struts MailReader example application provides a good basis for demonstration. Say you wanted to create a test for the LogonAction shown in Example 13-6.

Example 13-6. Struts example (MailReader) LogonAction
package org.apache.struts.webapp.example; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; public final class LogonAction extends BaseAction {     /**      * Name of username field ["username"].      */     static String USERNAME = "username";     /**      * Name of password field ["password"].      */     static String PASSWORD = "password";     // --------------------------------------------------- Protected Methods     /**      * <p>Confirm user credentials. Post any errors and return User object      * (or null).</p>      *      * @param database Database in which to look up the user      * @param username Username specified on the logon form      * @param password Password specified on the logon form      * @param errors ActionMessages queue to passback errors      *      * @return Validated User object or null      * @throws ExpiredPasswordException to be handled by Struts exception      * processor via the action-mapping      */     User getUser(UserDatabase database, String username,                            String password, ActionMessages errors)          throws ExpiredPasswordException {         User user = null;         if (database == null){             errors.add(                 ActionMessages.GLOBAL_MESSAGE,                 new ActionMessage("error.database.missing"));         }         else {             user = database.findUser(username);             if ((user != null) && !user.getPassword( ).equals(password)) {                 user = null;             }             if (user == null) {                 errors.add(                     ActionMessages.GLOBAL_MESSAGE,                     new ActionMessage("error.password.mismatch"));             }         }         return user;     }     /**      * <p>Store User object in client session.      * If user object is null, any existing user object is removed.</p>      *      * @param request The request we are processing      * @param user The user object returned from the database      */     void SaveUser(HttpServletRequest request, User user) {         HttpSession session = request.getSession( );         session.setAttribute(Constants.USER_KEY, user);         if (log.isDebugEnabled( )) {             log.debug(                 "LogonAction: User '"                     + user.getUsername( )                     + "' logged on in session "                     + session.getId( ));         }     }     // ------------------------------------------------------ Public Methods     /**      * Use "username" and "password" fields from ActionForm to retrieve a User      * object from the database. If credentials are not valid, or database      * has disappeared, post error messages and forward to input.      *      * @param mapping The ActionMapping used to select this instance      * @param form The optional ActionForm bean for this request (if any)      * @param request The HTTP request we are processing      * @param response The HTTP response we are creating      *      * @exception Exception if the application business logic throws      *  an exception      */     public ActionForward execute(         ActionMapping mapping,         ActionForm form,         HttpServletRequest request,         HttpServletResponse response)         throws Exception {         // Local variables         UserDatabase database = getUserDatabase(request);         String username = (String) PropertyUtils.getSimpleProperty(form,                 USERNAME);         String password = (String) PropertyUtils.getSimpleProperty(form,                 PASSWORD);         ActionMessages errors = new ActionMessages( );         // Retrieve user         User user = getUser(database,username,password,errors);         // Report back any errors, and exit if any         if (!errors.isEmpty( )) {             this.saveErrors(request, errors);             return (mapping.getInputForward( ));         }         // Save (or clear) user object         SaveUser(request,user);         // Otherwise, return "success"         return (findSuccess(mapping));     } }

This Action is used by the /SubmitLogon action defined in the struts-config.xml file:

<!-- Process a user logon --> <action    path="/SubmitLogon"            type="org.apache.struts.webapp.example.LogonAction"            name="LogonForm"           scope="request"           input="logon">     <exception               key="expired.password"              type="org.apache.struts.webapp.example.ExpiredPasswordException"              path="/ExpiredPassword.do"/> </action>

Now, you can create a test to verify that the LogonAction performs as expected. Example 13-7 presents a unit test that verifies the following:

  • If a valid username and password are entered, the correct User object is added to the session and control is forwarded to the "success" page.

  • If an invalid username and password are entered, an ActionError with the key of "error.password.mismatch" is generated and control is forwarded to the "logon" page.

Example 13-7. Unit test for the LogonAction
package com.oreilly.strutsckbk.ch13; import org.apache.struts.webapp.example.Constants; import org.apache.struts.webapp.example.User; import servletunit.struts.MockStrutsTestCase; public class SubmitLogonActionTest extends MockStrutsTestCase {     private static final String ACTION_PATH = "/SubmitLogon";          public SubmitLogonActionTest (String theName) {         super(theName);     }     public void testValidUserLogon( ) throws Exception {         addRequestParameter("username", "user");         addRequestParameter("password", "pass");         setRequestPathInfo(ACTION_PATH);         actionPerform( );              verifyNoActionErrors( );                  User user = (User) getSession( ).getAttribute(Constants.USER_KEY);         assertNotNull("User", user);         assertEquals("Username", "user", user.getUsername( ));         verifyForward("success");     }     public void testInvalidUserLogon( ) throws Exception {         addRequestParameter("username", "junk");         addRequestParameter("password", "bond");         setRequestPathInfo(ACTION_PATH);         actionPerform( );                  verifyActionErrors(new String[] {"error.password.mismatch"});                  verifyForward("logon");     } }

Each test adds request parameters corresponding to the username and password. The ACTION_PATH constant contains the URL of the action being tested. The actionPerform( ) method processes the request through the mock container. Upon completion of this method, you can check that the Action did what you expected. Because the MockStrutsTestCase extends the JUnit TestCase class, your unit test has full access to all of the assertion methods that JUnit provides. On top of that, Struts-TestCase adds additional methods for verifying Struts-specific behavior:


verify[No]ActionErrors

Checks that specific errors were generated or that none were generated


verify[No]ActionMessages

Checks that a specific set of action messages, identified by key, were sent or that none were sent


verifyForward

Checks that the Action forwarded to a specific ActionForward identified by logical name


verifyForwardPath

Checks that the Action forwarded to a specific URL


verifyInputForward

Checks that the controller forwarded to the path identified by the action mappings input attribute


verifyTilesForward

Checks that the controller forwarded to a specified logical forward name from the Struts configuration and a Tiles definition name from the Tiles configuration


verifyInputTilesForward

Checks that the controller forwarded to the defined input of a specified Tiles definition

My initial coding of the SubmitLogonActionTest used verifyInputForward( ), instead of verifyForward("logon"), to check that control was forwarded back to the appropriate page when invalid data was submitted. Unexpectedly, the verifyInputForward( ) assertion failed with the following message:

junit.framework.AssertionFailedError: was expecting '/logon'  but received '/Logon.do'

It took a second to realize that the struts-example configures the controller to treat the input attribute value as a local or global forward instead of a module-relative path:

<controller pagePattern="$M$P" inputForward="true"/>

Unfortunately, verifyInputForward( ) expects the value of the input attribute on the action element to be a module-relative path. Changing verifyInputForward( ) to verifyForward("logon") resolved the issue and the test passed.

You'll find another wrinkle in testing the Struts MailReader example that's worth exploring. Suppose you wanted to test an action related to the application's registration features. These actions are declared in the struts-config-registration.xml file, and not the standard struts-config.xml file. When you use an alternate configuration file like this, you have to tell StrutsTestCase to use it. The setConfigFile( ) method gives you this ability:

setConfigFile(java.lang.String pathname); setConfigFile(java.lang.String moduleName,java.lang.String pathname);

Use the first variation to specify the location of the nonstandard Struts configuration file if you aren't using modules; for the default module, use the second variation to specify the nonstandard Struts configuration file for a specific module.

Your test's setUp( ) method is the logical place to call this method. You can put other common code, required to initialize every test, in the setUp( ) method as well.

If you choose to override the setUp( ) method, you must call super.setUp( ). This base method performs some important initialization routines, and StrutsTestCase will not work if it isn't called.


Example 13-8 shows a test case for the EditRegistrationAction.

Example 13-8. A test using an alternate Struts configuration file
package com.oreilly.strutsckbk.ch13; import org.apache.struts.Globals; import servletunit.struts.MockStrutsTestCase; public class EditRegistrationActionTest extends MockStrutsTestCase {     private static final String ACTION_PATH = "/EditRegistration";          public EditRegistrationActionTest (String theName) {         super(theName);     }     public void setUp( ) throws Exception {         super.setUp( );         setConfigFile("/WEB-INF/struts-config-registration.xml");         setRequestPathInfo(ACTION_PATH);     }          public void testCreateRegistration( ) throws Exception {         addRequestParameter("action", "Create");         actionPerform( );         String token = (String) getRequest( ).getAttribute(                                   Globals.TRANSACTION_TOKEN_KEY);         assertNotNull(token, "Token was not saved");         verifyForward("success");     } }

This test shows how you can verify that attributes are stored, as expected, in the request. In this case, the EditRegistrationAction is supposed to store a Struts transaction token in the request.

See Also

StrutsTestCase provides two base classes for creating Action unit tests. MockStrutsTestCase is used for creating tests that can be run outside of a servlet container. The other base class, CactusStrutsTestCase, can be used for testing actions running in a live container using the Cactus (http://jakarta.apache.org/cactus) test framework. Recipe 13.7 shows this approach.

If you need to verify the actual HTML generated by an Action or JSP, you can use HttpUnit (http://httpunit.sourceforge.net). HttpUnit provides an object-oriented Java API that allows you to inspect the returned HTTP response.



    Jakarta Struts Cookbook
    Jakarta Struts Cookbook
    ISBN: 059600771X
    EAN: 2147483647
    Year: 2005
    Pages: 200

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net