StrutsTestCase 是一种用于测试Struts Actions的强大而且易于使用的测试框架。Struts和StrutsTestCase,结合传统的JUnit 测试,将会带来你高覆盖率的测试,从而提高产品的可靠性。
StrutsTestCase 是一种基于JUnit, 用于测试 Struts ,用于测试应用程序的Struts action 类的一种简单、高效的方式。
典型的J2ee应用程序的层次结构如图1所示:
? DAO层用于封装对数据库访问。
? 在DAO层,你会发现Hibernate 映射、类、查询、实体EJB或者其他的实体-关系持久技术。
? 业务逻辑层包含更多的高级的业务服务。在理想状态下,业务层独立于数据库实现。事务EJB在这层经常被使用。
? 表达层负责为用户显示应用程序数据和解释用户的请求。在Struts应用程序中,表达层经常使用JSP/JSTL显示数据,用Struts action处理用户的请求。
? 基本上,客户层是运行在客户机器上的web浏览器。客户端逻辑(比如:JS)有时候被放到这一层,虽然它是难以有效被测试的。
Figure 1. Typical J2EE architecture
DAO和业务层可以用经典的JUnit,或者其他一些JUnit扩展工具做测试。DbUnit就是一个的做数据库单元测试的很好选择。(更多DbUnit知识,查看Andrew Glover的“用DbUnit做高效的单元测试”)
另一方面,对struts的Action进行测试是很困难的。即使当业务逻辑很好的被限定在业务层,Struts action通常还是会包含很重要的数据验证、数据转换和数据流控制代码。不对Struts action 支路进行测试在代码的覆盖率上会有很多的不足。StrutsTestCase 会弥补这些不足。
对action层进行单元测试也会带来其他的好处:
? 视图层和业务层会更容易理解,更加简单和更清楚。
? 重构Action类会变得更容易。
? 避免多余、无用的action类。
? 测试能有助于Aciton层的文档的编写(在写jsp页面时候有作用)
这是测试驱动开发的常见的一些好处,并且这些好处是除了在Sturts Action层测试还是其他的一些地方都是通用的。
StrutsTestCase介绍
StrutsTestCase工程提供了一种在JUnit框架下测试struts action的灵活、便利的方法。你可以通过设置请求参数,检查在Action被调用后的输出请求或Session状态这种方式对Struts Action做白盒测试。
StrutsTestCase 提供了用框架模拟web容器的模拟测试方法也提供了真实的web容器(比如:Tomcat)下的测试方法。一般来说,我更喜欢模拟测试,因为它更加的轻便、运行更快,因而更经凑的开发。
所有的StrutsTestCase单元测试类都源于模拟测试的 MockStrutsTestCase或容器内测试的CactusStrutsTestCase。
在这里我们更关注mock测试,因为它需要更少的启动和更快的运行。
StrutsTestCase 实践
用StrutsTestCase做Action的测试,我们创建一个类,这个类继承类MockStrutsTestCase。这个类提供了建立模拟HTTP请求的方法。用这个HTTP请求调用Struts action,并且当这个action完成时检查应用程序的状态。
假设,一个带有符合查询功能函数的在线预定宾馆系统数据库。
查询函数是通过/search.do action类实现。
这个Action类体统一个基于特殊标准的符合查询功能,将通过这个功能得到的结果列表放置在request范围内的属性“results”中。
比如:
下面的请求URL将会显示所有法国的住宿地列表。
/search.do?country=FR
现在,我们用测试驱动的方法去实现这个方法。编写这个action类并且更新Struts的配置文件。
我们也写一个测试类测试这个aciton(空)类。
用严格的测试驱动的方法,首先,我们编写一个测试类,然后实现这个测试类的测试代码。实际情况下,标准的顺序根据被测试的代码不同而有所不同。
如下初始化测试类:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
}
在这里,我们设置调用setRequestPathInfo()的路径,增加了addRequestParameter()方法的请求参数。
接下来,我们调用这个带有actionPerform()方法的action类。这样会校验Struts配置并调用相应的action类,但是不能测试这个acition实际的功能是什么。为了知道这个action类的实际的功能,我们需要检查核对action类返回的结果。
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
}
这里我们核对三件事:
? 无ActionError消息(verifyNoActionErrors())
? 返回“success”转向路径。
? 结果集属性被放置在request范围内。
如果我们使用了tile,那么我们也要用以下的代码核查“success”转向是否指向正确的tile定义。
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyTilesForward("success",
"accommodation.list.def");
assertNotNull(request.getAttribute("results"));
}
实际应用中,我们可能执行特殊业务逻辑的测试。
比如:
假设results属性包含了100个宾馆对象,我们要确认所有的这个results列表包含了宾馆都是在法国。为了达到这个目的,我们需要做一个测试,所用的代码与标准的JUnit测试代码很相似。
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results
= (List) request.getAttribute("results");
assertEquals(results.size(), 100);
for (Iterator iter = results.iterator();
iter.hasNext();) {
Hotel hotel = (Hotel) iter.next();
assertEquals(hotel.getCountry,
TestConstants.FRANCE);
...
}
}
当你遇到更复杂的情况,你可能想测试一系列的action.
比如:
用户做一个所有在法国的宾馆的查询,单击其中一个宾馆以查看这个宾馆的详细信息。 假设,我们有一个用以显示宾馆详细信息的action类,如下调用:
/displayDetails.do?id=123456
使用StrutsTestCase,我们就能轻松的模拟上述的过程:先查询一个在法国的所有宾馆的列表,然后点击进去可以查看详细的宾馆信息。
如下:
public void testSearchAndDisplay() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results
= (List) request.getAttribute("results");
assertEquals(results.size(),100);
Hotel hotel = (Hotel) results.get(0);
setRequestPathInfo("/displayDetails.do");
addRequestParameter("id", hotel.getId());
actionPerform();
verifyNoActionErrors();
verifyForward("success");
Hotel hotel
= (Hotel)request.getAttribute("hotel");
assertNotNull(hotel);
...
}
测试Struts的错误处理
测试struts的错误处理也是很重要的。
假设,如果当一个错误的国家代码被定义的时候,我们的应用程序对此错误有很好的处理能力。
我们编写一个新的测试方法,此方法用verifyActionErrors()方法检查返回的struts 错误信息。
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors(
new String[] {"error.unknown,country"});
verifyForward("failure");
}
有时,我们想在ActionForm对象中直接进行数据验证。你可以按照如下的例子,使用getActionForm()方法。
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors(
new String[] {"error.unknown,country"});
verifyForward("failure");
SearchForm form = (SearchForm) getActionForm();
assertEquals("Scott", form.getCountry("XX"));
}
这里,我们验证了在出现一个错误的国家代码的时候,ActionForm能将此错误国家代码很好的保存。
定制测试环境
有时,重载setUp()方法是有用的,它将让你指定在无默认设置情况下的选项。
在这个例子中,我们使用不同的struts-config.xml文件,并且使得XML配置文件的验证无效:
public void setUp() {
super.setUp();
setConfigFile("/WEB-INF/my-struts-config.xml");
setInitParameter("validating","false");
}
第一级别的性能测试
测试一个action或一系列action是测试请求、响应时机是否合理的一种优秀的方法。通过Struts action测试,我们能够检查服务器端的性能(当然排除Jsp页面)。在单元测试阶段进行一些初始级别的性能测试是快速排除和隔离性能问题和避免在集成阶段性能衰退的明智之举。
这里是我用第一级别的Struts性能测试的一些基本规则:
? 尽可能的使用多的组合条件进行符合条件的查询。(以确认索引被正确的定义)
? 进行大容量的查询测试(此查询返回大量的结果)用以确认响应的次数和结果页面。
? 单独和重复的测试结合(以检查缓存的性能,如果缓存策略被使用)
一些开源的实验室帮助你进行性能的测试,比如:Mike Clark JUnitPerf。当然,这样的测试如果结合StrutsTestCase是有一定的困难的。在很多情况下,一个简单的计数器就是解决这个问题的窍门。以下就是一个简单但有效的进行第一级的新能测试的方法:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
long t0 = System.currentTimeMillis();
actionPerform();
long t1 = System.currentTimeMillis() - t0;
log.debug("Country search request processed in "
+ t1 + " ms");
assertTrue("Country search too slow",
t1 >= 100)
}
结论
通常,单元测试是敏捷编程的一部分,测试却动开发是其中特别的一部分。StrutsTestCase提供了一种简单有效的测试Struts action进行单元测试的方法,而对Struts action进行测试在Junit看来是非常困难的。