|
Recipe 13.6. Testing Your Actions with Mock ObjectsProblemYou want to unit test your Struts actions without running the application on an application server. SolutionUse 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. DiscussionThe 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:
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.
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) LogonActionpackage 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:
Example 13-7. Unit test for the LogonActionpackage 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:
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.
Example 13-8 shows a test case for the EditRegistrationAction. Example 13-8. A test using an alternate Struts configuration filepackage 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 AlsoStrutsTestCase 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. |
|