级别: 中级 |
Sunil Patil (sunilpatil@in.ibm.com)
软件工程师, IBM Software Labs
2005 年 2 月
本文将介绍 StrutsTestCase(STC)框架,解释如何用模拟方式和 Cactus 方式测试示例应用程序。作者 Sunil Patil 是 IBM 印度软件试验室的开发人员,他首先将介绍 STC,然后会带您遍历设置使用 STC 和测试各种 Struts 特性的环境的过程。还将演示如何在 STC 中同时使用 Cactus 和模拟方式。
注意:本文要求读者熟悉 Struts 框架。
StrutsTestCase(STC)框架是一个开源框架,用来测试基于 Struts 的 Web 应用程序。这个框架允许您在以下方面进行测试:
- 在 ActionForm 类中的验证逻辑(validate() 方法)。
- 在 Action 类中的业务逻辑(execute() 方法)。
- 动作转发(Action Forwards)。
- 转发 JSP。
STC 支持两种测试类型:
- Mock 方法 —— 在这种方法中,通过模拟容器提供的对象(HttpServletRequest、 HttpServletResponse 和 ServletContext),STC 不用把应用程序部署在应用服务器中,就可以对其进行测试。
- Cactus 方法 —— 这种方法用于集成测试阶段,在这种方法中,应用程序要部署在容器中,所以可以像运行其他 JUnit 测试用例那样运行测试用例。
示例应用程序
首先我们将逐步介绍示例 Struts 应用程序的创建,这个应用程序是测试的基础。可以用 Struts 自带的 struts-blank.war 或者自己喜欢的 IDE 来创建示例应用程序。示例应用程序中有一个登录页面,用户在这里输入用户名和口令。如果登录成功,用户会被重定向到成功页面。如果登录失败,那么用户会被重定向到登录页面。
选择本文顶部或底部的 Code 图标可以得到本文附带的源代码。
Login.jsp 页面
创建登录页面,如清单 1 所示:
<%@ 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> |
LoginActionForm.java 类
创建 LoginActionForm.java 类,如清单 2 所示:
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 } } |
在 validate() 方法中,需要检测用户是否输入了用户名和口令,因为这些字段是必需的。而且,还需要查询 USERDISABLED 表,确认用户的帐户没有被禁用。
LoginAction.java 类
接下来,要创建 LoginAction.java 类,如清单 3 所示:
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 } } |
在这里,execute() 方法用于验证用户名和口令是否有效。示例应用程序用 USER 表保存用户名和口令。如果用户的凭证有效,则会在请求范围内保存用户名,并把用户转到登录成功页面(Success.jsp)。
struts-config.xml 文件
创建 struts-config.xml 文件,如清单 4 所示:
<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> |
如果登录不成功,那么用户会被重新定向到登录页面。
Success.jsp 页面
创建 Success.jsp 页面,如清单 15 所示:
<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> |
在这里,可从属性范围中读取 userName 属性,并用它来欢迎已经登录的用户。
使用模拟对象方式
模拟测试是对应用程序进行单元测试的流行方式。如果是初次接触模拟测试方式,想了解更多的内容,那么请参阅参考资料。
设置模拟方式
要使用模拟方式,必须对示例应用程序做少许修改。首先要从编写模拟测试开始:
- 把 strutstest-2.1.*.jar 和 junit3.8.1.jar 添加到 classpath。
- 把 WEB-INF 文件夹添加到 classpath。
- 创建 MockLoginTestAction 类,它扩展了 MockStrutsTestCase 类。
- 运行单元测试用例。
现在就完成了对环境的设置,可以开始编写单元测试用例了。
空的用户名或口令
首先,需要验证用户是否没有输入用户名或口令,然后向用户显示适当的错误信息,并将用户重定向到登录页面。可以在 MockLoginTestAction 类中创建 testLoginActionFormError() 方法, 如清单 6 所示:
public void testLoginActionFormError()throws Exception{ setRequestPathInfo("/login"); actionPerform(); String[] actionErrors = {"username.required","password.required"}; verifyActionErrors(actionErrors); verifyInputForward(); } |
在编写 STC 测试用例时,要做的第一件事就是告诉 STC 要测试哪个 ActionMapping 类,在这里要测试 LoginAction,它被映射到 struts-config.xml 文件中的 "/login" 路径,因此我们必须调用 setRequestPathInfo("/login")。默认情况下,STC 在 /WEB-INF/ 文件夹中查找 struts-config.xml 文件。如果在 classpath 没有这个文件,就必须用 struts-config.xml 文件的完整路径调用 setConfigFile()。
现在可以执行测试用例了。首先要调用 actionPerform() 方法,把控制权传递给 Struts 框架,执行测试用例。一旦控制权从 actionPeform() 返回,就可以调用 verifyXXX() 方法,测试对程序的假设。在示例应用程序中,我们想测试一下,在没有用户名和口令的时候,调用 LoginAction 映射是否会利用出错信息 ActionErrors(用于 username.required 和 password.required)将用户重定向到登录页面。verifyInputForward() 方法检查这个事务的结果是否把用户重定向到动作映射的输入属性指定的页面,在这个例子中,该页面是 Login.jsp。
可以用 String 数组调用 verifyActionErrors(),该数组指出,作为这个事务的结果,应当在请求范围中设置哪些 ActionErrors。我们想设置 username.required、password.required 和 ActionErrors,所以创建了一个 String 数组来保存这些出错信息,并把它们发送给 verifyActionErrors() 方法。
STC 模拟方式如何工作
ActionServlet 在 Struts 框架中是一个控制器 servlet。当容器得到请求时,会把请求传递给 ActionServlet,由后者进行所有的请求处理。
STC 背后的基本想法是自行创建 ActionServlet 对象,而不是让容器来创建它,然后再调用对象上的适当方法。ActionServlet 在初始化时需要 ServletContext 和 ServletConfig 对象,在请求处理时需要 HttpServletRequest 和 HttpServletResponse 对象。STC 创建这些类的模拟对象,并把它们传递给 Struts。
MockStrutsTestCase 是一个扩展了 junit.framework.TestCase 类的 JUnit 测试用例,所以每个测试用例都会执行 setup() 方法。在 MockStrutsTestCase 对象的 setup() 方法中,STC 创建 ActionServlet 对象和其他必需的模拟对象。
在调用 setRequestPathInfo() 或 addRequestParameter() 方法时,会调用模拟 HttpServletRequest 对象的适当方法。在 HttpServletRequest 的模拟实现中,会把这条信息保存在适当的设置状态。所以,如果调用 addRequestParameter("name","value"),模拟的 HttpServletRequest 对象会保存它,然后,在 Struts 调用 request.getParameter("name") 时,用 "value" 作为返回值。
在恰当地完成 HttpServletRequest 初始化之后,就可以调用 actionPerform() 方法把控制权传递给 Struts。actionPerform() 方法调用 ActionServlet 的 doPost() 方法传递 HttpServletRequest 和 HttpServletResponse 的模拟实现。
在 ActionServlet 的 doPost() 方法中,处理请求的方式与其他 Struts 请求的处理方式类似,区别是直到执行 ActionForward JSP 组件之前才停止请求处理。在这个阶段,模拟对象的状态会被修改,以指出已经保存 ActionErrors 或 ActionMessages,或者指出由此生成的 ActionForward 是什么。
一旦控制权从 control returns from the actionPerform() 方法返回,就可以调用适当的 verifyXXX() 方法(检测模拟对象的状态)来检查各种假设是否成立。
测试禁用的用户
LoginActionForm 类的 isUserDisabled() 方法存在一个问题。在这个方法中,是通过查询 USERDISABLED 表来找出用户帐户是否被禁用。但是在当前的环境下,我们不想把时间浪费在设置和查询数据库上。
请记住,我们的目标是检查应用程序的 Struts 部分,而不是检查数据库的交互代码。为了测试数据库交互代码,可以从若干个可用工具中选择一个工具,例如 DBUnit。针对这一情况的最佳方案应当是创建一个 LoginActionForm 类的子类,并重写其中的 isUserDisabled() 方法。这个方法将根据输入参数的值判断是返回 true 还是返回 false。
比如在这个例子中,方法会一直返回 true,除非用 disabledUser 作为输入参数调用它。现在只应当在单元测试阶段使用这个方法,而主程序 LoginActionForm 不应当知道这一点。针对这个需求,我创建了 STCRequestProcessor,它扩展了 RequestProcessor。它允许向 Action 和 ActionForm 类中插入模拟实现。
要使用 STCRequestProcessor,需要修改 struts-config.xml,如清单 7 所示:
清单 7. struts-config.xml 文件 <controller> <set-property property="processorClass" value="com.sample.util.STCRequestProcessor"/> </controller> </code> |
这一行指出 Struts 用 STCRequestProcessor.java 作为 RequestProcessor。不要忘记,在容器中部署应用程序部署时要删除这些行。
接下来是创建 LoginActionForm 的模拟类,如清单 8 所示:
清单 8. MockLoginActionForm.java 类 public class MockLoginActionForm extends LoginActionForm { public boolean isUserDisabled(String userName) { if (userName != null && userName.equals("disableduser")) return true; return false; } } |
isUserDisabled() 方法检查用户名是否为 "disableduser"。如果是,则应当返回 true;否则应当返回 false。
接下来要 创建一个测试用例,对禁用用户进行测试,如清单 9 所示:
清单 9. testDisabledUser() 方法 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); } |
STCRequestProcessor.addMockActionForm() 方法把 MockLoginActionForm 作为 LoginActionForm 的模拟实现插进来。addRequestParameter() 方法设置用户名和口令这两个请求参数。一旦控制权从 actionPerform() 返回,就可以调用 verifyActionErrors() 验证是否利用 user.disabled 出错信息将用户重定向到输出页面。
测试无效登录
测试用例要测试 LoginAction 类的 execute() 方法内部的业务逻辑。execute() 方法调用同一个类的 isValidUser() 方法,该方法接下来会查询 USER 表,查看用户名和口令组合是否有效。现在,因为我们不想在测试阶段查询真正的数据库,所以要创建一个 LoginAction 类的模拟子类,重写 isValidUser() 方法,如清单 10 所示:
public class MockLoginAction extends LoginAction { public boolean isValidUser(String userName, String password) { if( userName.equals("ibmuser") && password.equals("ibmpassword")) return true; return false; } } |
如果用户名是 "ibmuser",口令是 "ibmpassword",则 MockLoginAction 类的 isValidUser() 方法将返回 true。调用 STCRequestProcessor.addMockAction() 方法把 MockLoginAction 插入 LoginAction,如清单 11 所示:
清单 11. testInvalidLogin() 方法 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(); } |
在这个测试用例中,插入了 LoginAction 和 LoginActionForm 的模拟实现,避免数据库查询,接着要设置用户名和口令参数。在控制权从 actionPerform() 返回之后,就可以检查是否利用 "invalid.login" 这条出错信息把用户重定向到登录页面。
测试有效登录
现在是时候来验证在用户输入正确的用户名和口令时,是否用成功页面欢迎用户,如清单 12 所示:
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"); } |
这一代码段首先在请求参数中把用户名设置为"ibmuser",并把口令设置为 "ibmpassword",然后调用 actionPerform()。在执行 actionPerform() 方法时,需要调用 verifyForward() 方法,检查用户是否被重定向到成功页面。它还调用了 verifyNoActionErrors() 方法,以验证在这个事务中没有出现 ActionErrors。
模拟的优势与不足
使用模拟方式有一些优势。这种方式比较快,因为不必为了每个更改而启动和停止容器。另一方面,因为没有使用真正的容器,所以可能无法验证监听器或过滤器带来的副作用。而且,因为没有执行 ActionForward JSP 组件,所以也无法发现 JSP 中的错误。
Cactus 方式
Cactus(容器内)是集成测试阶段的一种流行测试方法。这里不对它进行详细介绍 Cactus,有关的更多信息,请参阅参考资料。
Cactus 方式的设置
要设置 Cactus,需要将 cactus.1.6.1.jar 和 aspectjrt1.1.1.jar 复制到 classpath 中。
Cactus 需要在 Web 应用程序中配置两个 servlet,所以必须在 web.xml 文件中声明它们,如清单 13 所示:
清单 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> |
接下来要创建 cactus.properties 文件,并把它放在 classpath 中,如下所示:
cactus.contextURL = http://localhost:9080/sample1 cactus.servletRedirectorName = ServletRedirector |
本文使用 WebSphere Studio 内置的测试环境来运行测试用例,所以可以从 http://localhost:9080/sample1 访问示例应用程序。请确保把这个路径修改成指向 Web 应用程序实际部署位置的路径。
接下来要创建一个类,扩展 CactusStrutsTestCase。因为在模拟和 Cactus 方式中可以使用相同的测试用例,所以可以在这个类中复制 MockLoginActionTest 的内容。在选中的容器中构建并部署这个应用程序。
最后,把 jdbc/ds1 配置成数据源。
STC Cactus 方法的工作原理
在使用 Cactus 测试应用程序的时候,必须把应用程序部署在 Web 容器中,还要在容器外面用 JUnit 测试用例的形式运行 Cactus 测试用例。在运行 Cactus 单元测试时,它会为类中的每个测试用例方法都创建并执行一个针对 URL 的HTTP 请求,URL 由 cactus.properties 文件中 cactus.contextURL 参数指定。
在示例应用程序的例子中,在执行 testDisableUser 时,会创建并执行以下请求:
http://localhost:9080/sample1/ServletRedirector?Cactus_TestMethod=testDisabledUser&Cactus_TestClass= com.sample.test.CactusLoginActionTest&Cactus_AutomaticSession=true&Cactus_Service=CALL_TEST |
这个请求会调用 ServletTestRedirector servlet(作为示例 Web 应用程序的一部分部署)。在 ServletTestRedirector 中,Cactus 从 Cactus_TestClass 请求参数中查找测试用例类的名称,并调用 Cactus_TestMethod 参数指定的方法。在执行这个方法之后,就会以 HTTP 响应的方式把结果返回 Cactus 测试类,这个类将执行一个外部容器。
此外,在 testDisabledUser() 方法中的 CactusStrutsTestCase 的容器内(in-container)版本得到控制时(在本文的示例中是 CactusLoginActionTest),STC 会调用 actionPerform() 方法,该方法将创建 ActionServlet、ServletContext 和 ServletConfig 对象的实例。STC 还在包装器中包装了当前的请求和响应。然后它调用 ActionServlet 的方法 doPost(),该方法使用的参数是这些包装的 ServletRequest 和 ServletResponse 对象。然后 Struts 会像平常一样处理请求。
通过使用 Cactus 方式,就可以调用 processRequest(true) 方法告诉 STC 验证转发 JSP,从而执行和测试转发的 JSP,以确保不会抛出任何编译和运行时错误。
一旦控制权从 actionPerform() 返回,就可以调用各种 verifyXXX() 方法检验假设是否成立。
测试转发 JSP 的错误
修改 testVaidLogin() 方法,测试 Success.jsp,保证它没有编译时错误或运行时错误, 如清单 14 所示:
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"); } |
还要修改 Success.jsp,添加以下几行,让它抛出 RunTimeException 异常:
<% throw new RuntimeException("test error"); %> |
现在,当运行这个测试用例时,testValidLogin() 会创建并执行数据库查找,检查用户帐户是否禁用,用户名和口令是否有效。如果测试失败,则表明在执行 Success.jsp 时遇到了运行时错误。
Cactus 的优势与不足
使用 Cactus 当然有优势,但是困难也不少。从正面来说,它允许测试 JSP 页面的编译和运行时错误,还允许测试数据访问代码。从负面来说,这种方式要求把应用程序部署在容器中,然后每做一次修改都要启动和停止容器,这使 Cactus 成为一种较慢的模拟方式。
结束语
单元测试提供了很多好处。除了让人确信代码按照设计的方式工作之外,测试还是造就优秀文档的原因。而且,在设计类和接口时,单元测试还提供了一个优秀的反馈机制。最后,单元测试对于管理变化也很有帮助。如果在对代码进行更改之后,代码通过了所有单元测试,那么就可以确信这些更改是安全的。
不幸的是,许多开发人员放弃了单元测试,因为他们要花太多时间来编写测试代码。但是通过使用 STC 的模拟方式,可以把通常花费在设置特定领域(例如数据库和容器)开发环境上的大量时间节省下来。因为不必每次都重新启动和停止容器,所以 STC 还有助于迅速测试变化。一旦代码稳定下来,能够通过所有测试用例,那么只要改变一下测试用例的父类,就可以将它用于集成测试。在集成阶段使用 Cactus 还允许您自动化集成测试过程。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
- Struts, an open source MVC implementation(developerWorks,2001 年 2 月)的作者是 Malcolm Davis,这篇文章提供了一份优秀的 Stuts 指南。
- Writing a simple Struts application using WebSphere Studio V5(developerWorks,2003 年 2 月)的作者是 Colin Yu 和 Jane Fung,这篇文章展示了如何用 WebSphere Studio V5.0 内置的支持创建简单的 Struts 应用程序。
- "Developing and Unit Testing with Open Source Apache Cactus Framework Tools in WebSphere Studio Application Developer"(WebSphere Developer Technical Journal,2002 年 6 月)的作者是 Sheldon Wosnick,这篇文章展示了如何在 WebSphere Studio 中设置 Cactus 框架。
- Unit testing with mock objects(developerWorks,2002 年 11 月)的作者是 Alexander Day Chaffee 和 William Pietri,这篇文章提供了关于模拟测试的宝贵信息。
- 在 developerWorks Java 技术专区可以找到数百篇有关 Java 各个方面的技术文章。
- 请访问 Developer Bookstore,获得技术图书的完整列表,其中包括数百本 Java 相关主题的书籍。
下载
![]() |
Name | ![]() | Size | ![]() | Download method | ![]() | ||||||
sample1.zip | ![]() | 21 KB | ![]() |
![]() | |||||||||||
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
![]() |
关于作者 Sunil Patil 是 IBM 印度 Lotus 部门的软件工程师。他有将近 5 年的 J2EE 经验。 |