虽然 Struts 正在慢慢退出 Web 框架的历史舞台,但它的遗产仍然存在,存在的形式主要是需要测试和维护的应用程序。这个月,Andrew Glover 向您介绍如何使用 JUnit 的 StrutsTestCase、DbUnit 以及在这个系列中迄今为止学到的一些工具,把以质量为中心的方法用于 Struts 上的测试(可以这么说)。
基于 Java™ 的 Web 开发领域近出现了丰富的竞争性技术。启动新项目的开发人员可以在许多不同的框架之间进行选择,包括 JavaServer Faces、Tapestry、Shale、Grails 和 Seam (只列举众多机灵的名称中的几个)。很快,我们可以通过 JRuby 框架在 Java 编程中使用 Ruby on Rails 了!
但在不远的过去,只有一个 Java Web 开发框架卓然而立。Struts 是第一个在 Java 世界掀起风暴的框架,而且多年以来,好像是如果一个项目不用 Struts 构建没有前途一样。没有 Struts 经验的 Java 开发人员很稀少,也很不幸,像的开发人员没有听说过 Ruby on Rails 一样。
提高代码质量
不要错过 Andrew 的附带 讨论组 ,可以得到迫切问题的答案。
即使 Struts 正慢慢地从舞台中央退去(原来的基本框架,现在叫做 Struts 1,似乎正在退出 Web 框架的历史舞台),但它的遗产仍然存在,既以 Shale (请参阅 参考资料)的形式存在,又以运行在世界各地的成千上万的遗留应用程序的形式存在。因为许多企业宁愿测试和维护这些应用程序而不愿意花钱重新编写它们,所以理解 Struts 应用程序的一些缺陷,以及如何围绕它们进行重构,是个好主意。
这个月,我要把以质量为核心的方法用于 Struts 应用程序的测试场景。结合现实,这个场景围绕着普遍的 Struts 构造:深受喜爱的 Action 类。
1、2、3,行动!
Struts 的革新之一是把 Web 开发从 Servlet 移进了 Action 类。这些类包含业务逻辑,以 JavaBean 的形式(通常叫做 ActionForm)把数据传送到 JSP。然后 JSP 处理应用程序视图。Struts 到 MVC 的方法非常容易掌握,以至于许多开发团队冒失地闯进去,而很少考虑与 Action 相关的长期设计和维护问题。
测试和复杂性
我已经发现,在开发人员的测试和代码的复杂性之间存在强烈的相关性:没有其中一个的地方,通常也没有另一个。高度复杂的编码难于测试,结果是很少有人会真正为它编写测试。反过来,编写测试可以降低代码的复杂性。因为给复杂代码编写测试更困难,而且因为会边走边测试,所以会发现自己朝着更简单的代码构造前进。如果代码太复杂,而且知道不得不测试它,您可能会在测试之前对复杂性进行重构。不论如何看待,为不那么简单的代码编写测试是消灭代码复杂性的好实践。
虽然在那个时候(过去的自由时光啊)可能没人想过,但 Struts Action 类通常成为复杂性的保护所。像在老的 EJB 架构中声名狼籍的会话 Facade 一样,Action 类会成为特定业务过程的严格伪装,或者通过直接调用 EJB,通过打开数据库连接,或者通过调用其他高度依赖的对象。Action 类还有输出耦合(通过 java.servlet API 包中的对象,例如 HttpServletRequest 和 HttpServletResponse),从而极难把它们隔离出来测试。
隔离出来测试 Action 类的困难意味着它们可以很容易变得相当复杂 —— 特别是当它们变成越来越深入地与遗留框架耦合的时候。现在我们来看这个困难在真实的遗留应用程序场景中作用的情况。
测试挑战
即使简单的 Struts Action 类也会是个测试挑战。例如,以清单 1 中的 execute() 方法为例;它看起来足够简单,可以测试,但是真的么?
清单 1. 这个方法看起来容易测试……
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 = DataAclearcase/" target="_blank" >ccessUtils.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); }
图 1. Action 类的输出耦合
但是,像在图 1 中可以看到的,在试图隔离 ChangePasswordAction 类并检验 execute() 方法时,该类给出了一些有代表性的挑战。为了有效地测试 execute() 方法,必须处理三层耦合。首先,到 Struts 自身的耦合;其次,Servlet API 代表一个障碍;后,到业务对象包的耦合,进一步检查业务对象包,还会有数据访问层使用 Hibernate 和 Spring。
每种情况一个 mock?
即使在我编写本文时,我还可以听到开发人员的嘲笑者 认为我的测试问题通过明智地使用 mock 对象能轻易解决。可以 用 mock 对象创建一级隔离,它会形成更容易的测试;但是,我要说的是,把目标对象通过 mock 排除所需要的付出级别,比起承认隔离测试困难所需要的付出,要多得多。在这种情况下,我会采用在更高层次上的测试,这级测试有时叫做集成测试。
对于更高的复杂性,请注意 清单 1 中的代码如何把 aForm 参数转换成 ChangePasswordForm 对象,它是 Struts ActionForm 类型。这些 JavaBeans 有一个 validate 方法,这个方法由 Struts 在调用 Action 类的 execute() 方法之前调用。
犯错误太容易了
在清单 2 中,可以看到所有这个复杂性会在哪里发生。ChangePasswordForm 的 validate() 方法的代码片段演示了保证两个属性(newPassword1 和 newPassword2)不为空并彼此相等的简单逻辑。但是,如果 Struts 发现 errors 集合(类型为 ActionErrors)包含一些 ActionError 对象,会沿着错误路径走,例如带着出错消息重新显示 Web 页面。
清单 2. 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")); }}
清单 1 和 清单 2 的代码不特殊也不特定于某个领域。它是无数应用程序中都包含的简单口令修改逻辑。如果正在测试 Struts 遗留应用程序,将不得不花些时间处理口令逻辑,但是如何用可重复的方式测试它呢?