用“test-only”行为增强单元测试
级别:中等
Nicholas Lesiecki (ndlesiecki@apache.org)
主要软件工程师,eBlox, Inc.
在开发过程中结合了单元测试的程序员都了解这样做带来的好处:代码更简洁,敢于重构、速度更快。但即便是最执著的单元测试者,在碰到测试行为依靠于系统状态的类的情况时,也会显得信心不足。Nicholas Lesiecki 是一名受人尊敬的 Java 程序员,也是 XP 社区的领导者,他将介绍围绕测试案例隔离的问题,并向我们展示如何使用模拟对象(mock object)和 ASPectJ 来开发精确和健壮的单元测试。请通过单击文章顶部或底部的讨论,在讨论论坛与作者和其它读者分享您对本文的看法。
最近,人们对极端编程(Extreme Programming,XP)的关注已经扩大到它的一个最具可移植性的应用上:单元测试和最初测试设计。因为软件工作室已经开始采用 XP 的开发方法,我们可以看到,因为有了一套全面的单元测试工具,很多开发者的开发质量和速度都得到了提高。但编写好的单元测试耗时费力。因为每个单元都与其它单元合作,所以编写单元测试可能需要大量的设置代码。这使得测试变得更加昂贵,而且在特定情况下(比如代码充当远程系统的客户机时),这样的测试可能几乎无法实现。
在 XP 中,单元测试弥补了集成测试和验收测试的不足。后两种测试类型可能由独立的小组进行,或者作为独立的活动进行。但是单元测试是与要测试的代码同时编写的。面对日益逼近的截止期限和令人头痛的单元测试带来的压力,我们很可能随便编写一个测试了事,或者完全放弃测试。因为 XP 依靠于积极的动机和自给自足的习惯,所以 XP 过程(和项目!)的最佳利益就是使测试保持集中和易于编写。
所需背景
本文的重点是 AspectJ 的单元测试,所以文章假定您熟悉基本的单元测试方法。假如您不熟悉 AspectJ,那么在继续之前阅读一下我对 AspectJ 的介绍很可能会对您有所帮助(请参阅参考资料)。这里所说的 AspectJ 方法不是非常复杂,但面向 aspect 的编程却需要一点时间去习惯。为了运行示例,您需要在测试机器上安装 Ant。不过您不需要具有任何非凡的 Ant 专门技术(超出基本安装所需的技术)来运行示例。要了解更多信息或者下载 Ant,请参阅参考资料部分。
模拟对象可以帮助您解决这种进退两难的局面。模拟对象测试用只用于测试的模拟实现来替代和域相关的东西。然而,这种策略的确在某些情况下带来了技术上的难题,比如远程系统上的单元测试。AspectJ 是 Java 语言的一种面向 aspect 的扩展,它答应我们在传统的面向对象方法失败的地方代之以 test-only 行为,从而用其它方法进行单元测试。
在本文中,我们将讨论一种编写单元测试既困难又合乎需要的常见情况。我们将从为一个基于 EJB 的应用程序的客户机组件运行单元测试开始。我们将使用这个示例作为出发点,来讨论在远程客户机对象上进行单元测试时可能出现的一些问题。为了解决这些问题,我们将开发两个新的依靠于 AspectJ 和模拟对象的测试配置。看到文章末尾时,您就应该对常见的单元测试问题和它们的解决方案有所了解,还应该初步了解 AspectJ 和模拟对象测试提供的一些有趣的可能性。
为了运行我们在全篇文章中将使用的这些代码示例,您现在可能希望安装示例应用程序。
单元测试示例
示例由 EJB 客户机的一个测试组成。本案例研究中提出的很多问题都适用于调用 Web 服务的代码、调用 JDBC 的代码、甚至本通过虚包调用的本地应用程序“远程”部分的代码。
服务器端的 CustomerManager EJB 执行两种功能:它查找客户名并向远程系统注册新客户名。清单 1 展示了 CustomerManager 公开给客户机的接口:
清单 1. CustomerManager 的远程接口
public interface CustomerManager extends EJBObject {
/**
* Returns a String[] representing the names of customers in the system
* over a certain age.
*/
public String[] getCustomersOver(int ageInYears) throws RemoteException;
/**
* Registers a new customer with the system. If the customer already
* exists within the system, this method throws a NameExistsException.
*/
public void register(String name)
throws RemoteException, NameExistsException;
}
客户机代码名为 ClientBean,它本质上将公开相同的方法,将实现这些方法的任务交给 CustomerManager,如清单 2 所示。
清单 2. EJB 客户机代码
public class ClientBean {
private Context initialContext;
private CustomerManager manager;
/**
* Includes standard code for referencing an EJB.
*/
public ClientBean() throws Exception{
initialContext = new InitialContext();
Object obj =
initialContext.lookup("java:comp/env/ejb/CustomerManager");
CustomerManagerHome managerHome = (CustomerManagerHome)obj;
/*Resin uses Burlap instead of RMI-IIOP as its default
* network protocol so the usual RMI cast is omitted.
* Mock Objects survive the cast just fine.
*/
manager = managerHome.create();
}
public String[] getCustomers(int ageInYears) throws Exception{
return manager.getCustomersOver(ageInYears);
}
public boolean register(String name) {
try{
manager.register(name);
return true;
}
catch(Exception e){
return false;
}
}
}
我有意将这个单元写得简单一点,这样我们就可以将精力集中在测试上。ClientBean 的接口与 CustomerManager 的接口只有一点点不同。与 ClientManager 不同,ClientBean 的 register() 方法将返回一个布尔值,而且在客户已经存在的时侯不会抛出异常。这些就是好的单元测试应该验证的功能。
清单 3 所示的代码将实现 ClientBean 的 JUnit 测试。其中有三个测试方法,一个是 getCustomers() 的,另外两个是 register() 的(其中一个是成功的,另一个是失败的)。测试假定 getCustomers() 将返回一个有 55 个条目的列表,register() 将为 EXISTING_CUSTOMER 返回 false,为 NEW _CUSTOMER 返回 true。
清单 3. ClientBean 的单元测试
//[...standard JUnit methods omitted...]
public static final String NEW_CUSTOMER = "Bob Smith";
public static final String EXISTING_CUSTOMER = "Philomela Deville";
public static final int MAGIC_AGE = 35;
public void testGetCustomers() throws Exception {
ClientBean client = new ClientBean();
String[] results = client.getCustomers(MAGIC_AGE);
assertEquals("Wrong number of client names returned.",
55, results.length);
}
public void testRegisterNewCustomer() throws Exception{
ClientBean client = new ClientBean();
//register a customer that does not already exist
boolean couldRegister = client.register(NEW_CUSTOMER);
assertTrue("Was not able to register " + NEW_CUSTOMER, couldRegister);
}
public void testRegisterExistingCustomer() throws Exception{
ClientBean client = new ClientBean();
//register a customer that DOES exist
boolean couldNotRegister = ! client.register(EXISTING_CUSTOMER);
String failureMessage = "Was able to register an existing customer ("
+ EXISTING_CUSTOMER + "). This should not be " +
"possible."
assertTrue(failureMessage, couldNotRegister);
}
假如客户机返回了预期的结果,那么测试就将通过。虽然这个测试非常简单,您还是可以轻易地想象同样的过程会如何应用到更复杂的客户机上,比如根据对 EJB 组件的调用生成输出的 servlet。
假如您已经安装了样本应用程序,那么请试着用示例目录中的命令 ant basic 运行这个测试若干次。
依靠数据的测试的问题
在运行了几次上述测试后,您就会注重到结果是不一致的:有时候测试会通过,有时候不会通过。这种不一致性归咎于 EJB 组件的实现 ? 而不是客户机的实现。示例中的 EJB 组件模拟了一个不确定的系统状态。测试数据中的不一致性显示出了在实现简单的、以数据为中心的测试时将出现的实际问题。另一个比较大的问题就是轻易重复测试工作。我们将着手解决这里的两个问题。
数据治理
克服数据中不确定性简单的方法就是治理数据的状态。假如我们能够设法在运行单元测试之前保证系统中有 55 条客户记录,那么我们就可以确信 getCustomers() 测试中的任何失败情况都可以表明代码中有缺陷,而不是数据问题。但是治理数据状态也会带来它自己的一些问题。您必须在运行每个测试之前确保系统对于特定测试处于正确的状态。假如您缺乏警惕,那么其中一个测试的结果就可能以某种方式改变系统的状态,而这种方式将使下一个测试失败。
为了应付这种负担,您可以使用共享设置类或批输入进程。但这两种方法都意味着要对基础结构作出很多投入。假如应用程序在某种类型的存储设备上持久化它的状态,您可能还会碰到更多问题。向存储系统添加数据可能很复杂,而且频繁的插入和删除可能使测试的执行非常缓慢。
高级测试
本文将集中讨论单元测试,然而集成测试或功能测试对快速的开发和较高的质量同样重要。实际上,这两种类型的测试是互补的。高级测试将验证系统的端对端完整性,而低级单元测试将验证单独组件。两种测试在不同情况下都是有用的。举例来说,功能测试可能通过了,但单元测试却找出了一个只在很少情况下才会出现的错误。反之亦然:单元测试可能通过了,而功能测试却显示各单独组件没有被正确地连在一起。有了功能测试,进行依靠数据的测试就更有意义,因为它的目标是验证系统的聚集行为。
有时候情况比碰到状态治理的问题还要糟糕,那就是完全无法实现这种治理。当您为第三方服务测试客户机代码时,您就可能发现自己处于这种情况下。只读类型的服务可能不会将改变系统状态的能力公开,或者您可能因为商业原因失去了插入测试数据的信心。举例来说,向活动的处理队列发送测试命令就很可能是个糟糕的想法。
重复的工作
即便您可以完全控制系统状态,基于状态的测试还是可以产生不需要的重复测试工作 ? 而且您不希望第二次编写相同的测试。
让我们将测试应用程序作为示例。假如我控制 CustomerManager EJB 组件,那么我就已经拥有了一个可以验证组件行为正确性的测试。我的客户机代码实际上并不执行任何与向系统添加新的客户相关的逻辑;它只是将操作交给 CustomerManager。那么,我为什么要在这里重新测试 CustomerManager 呢?
假如某个人改变了 CustomerManager 的实现以使其对相同数据作出不同响应,我就必须修改两个测试,从而跟踪改变。这有一点过耦合测试的味道。幸运的是,这样的重复是不必要的。假如我可以验证 ClientBean 与 CustomerManager 正确通信的话,我就有足够证据证实 ClientBean 是按其工作方式工作的。模拟对象测试恰恰答应您执行这种验证。
模拟对象测试
模拟对象使单元测试不会测试太多内容。模拟对象测试用模拟实现来代替真正的合作者。而且模拟实现答应被测试的类和合作者正确交互的简单验证。我将用一个简单的示例来演示这是如何实现的。
我们测试的代码将从客户机-服务器数据治理系统删除一个对象列表。清单 4 展示了我们要测试的方法:
清单 4. 一个测试方法
public interface Deletable {
void delete();
}
public class Deleter {
public static void delete(Collection deletables){
for(Iterator it = deletables.iterator(); it.hasNext();){
((Deletable)it.next()).delete();
}
}
}
简单的单元测试就可能创建一个真正的 Deletable,然后验证它在调用 Deleter.delete() 后将消失。然而,为了使用模拟对象测试 Deleter 类,我们编写了一个实现 Deletable 的模拟对象,如清单 5 所示:
清单 5. 一个模拟对象测试
public class MockDeletable implements Deletable{
private boolean deleteCalled;
public void delete(){
deleteCalled = true;
}
public void verify(){
if(!deleteCalled){
throw new Error("Delete was not called.");
}
}
}
下面,我们将在 Deleter 的单元测试中使用模拟对象,如清单 6 所示:
清单 6. 一个使用模拟对象的测试方法
public void testDelete() {
MockDeletable mock1 = new MockDeletable();
MockDeletable mock2 = new MockDeletable();
ArrayList mocks = new ArrayList();
mocks.add(mock1);
mocks.add(mock2);
Deleter.delete(mocks);
mock1.verify();
mock2.verify();
}
在执行时,该测试将验证 Deleter 成功地调用集合中每个对象上的 delete()。模拟对象测试按这种方式精确地控制被测试类的环境,并验证单元与它们正确地交互。
模拟对象的局限性
面向对象的编程限制了模拟对象测试对被测试类的执行的影响。举例来说,假如我们在测试一个稍微不同的 delete() 方法 ? 也许是在删除一些可删除对象之前查找这些对象的列表的方法 ? 测试就不会这么轻易地提供模拟对象了。下面的方法使用模拟对象可能很难测试:
清单 7. 一个很难模拟的方法
public static void deleteAllObjectMatching(String criteria){
Collection deletables = fetchThemFromSomewhere(criteria);
for(Iterator it = deletables.iterator(); it.hasNext();){
((Deletable)it.next()).delete();
}
}
模拟对象测试方法的支持者声称,像上面这样的方法应该被重构,以使其更加“易于模拟”。这种重构往往会产生更简洁、更灵活的设计。在一个设计良好的系统中,每个单元都通过定义良好的、支持各种实现(包括模拟实现)的接口与其上下文进行交互。
但即便在设计良好的系统中,也有测试无法轻易地影响上下文的情况出现。每当代码调用可全局访问的资源时,就会出现这种情况。举例来说,对静态方法的调用很难验证或替换,就像使用 new 操作符进行对象实例化的情况一样。
模拟对象对全局资源不起作用,因为模拟对象测试依靠于用共享通用接口的测试类手工替换域类。因为静态方法调用(和其它类型的全局资源访问)不能被覆盖,所以不能用处理实例方法的方式来“重定向”对它们的调用。
您可以向清单 4 中的方法传送任何 Deletable;然而,因为无法在真正类的地方装入不同的类,所以您不能使用 Java 语言的模拟方法调用替换静态方法调用。
一个重构示例
有些重构经常能够使应用程序代码向良好的解决方案发展,这种解决方案也可以轻易地测试 ? 但事情并不总是这样。假如得出的代码更难维护或理解,为了能够测试而进行重构并没有意义。
EJB 代码可能更加难于重构为答应轻易地模拟测试的状态。举例来说,易于模拟的一种重构类型将改变下面这种代码:
//in EJBNumber1
public void doSomething(){
EJBNumber2 collaborator = lookupEJBNumber2();
//do something with collaborator
}
改为这种代码:
public void doSomething(EJBNumber2 collaborator){
//do something with collaborator
}
在标准的面向对象系统中,这个重构示例答应调用者向给定单元提供合作者,从而增加了灵活性。但这种重构在基于 EJB 的系统中可能是不需要的。由于性能原因,远程 EJB 客户机需要尽可能多地避免远程方法调用。第二种方法需要客户机首先查找,然后创建 EJBNumber2(一个与若干远程操作有关的进程)的实例。
另外,设计良好的 EJB 系统倾向于使用“分层”的方法,这时客户机层不需要了解实现细节(比如 EJBNumber2 的存在等)。获取 EJB 实例的首选方法是从 JNDI 上下文查找工厂(Home 接口),然后调用工厂上的创建方法。这种策略给了 EJB 应用程序很多重构代码样本需要的灵活性。因为应用程序部署者可以在部署时在完全不同的 EJBNumber2 实现中交换,所以系统的行为可以轻易地进行调整。然而,JNDI 绑定不能轻易地在运行时改变。因此,模拟对象测试者面临两种选择,一是为了在 EJBNumber2 的模拟中交换而重新部署,二是放弃整个测试模型。
幸运的是,AspectJ 提供了一个折衷方法。
AspectJ 增加灵活性
AspectJ 能够在“每测试案例”的基础上提供对上下文敏感的行为修改(甚至在通常会禁止使用模拟对象的情况下)。AspectJ 的联接点模型答应名为 aspect 的模块识别程序的执行点(比如从 JNDI 上下文查找对象),并定义执行这些点的代码(比如返回模拟对象,而不是继续查找)。
aspect 通过 pointcut 识别程序控制流程中的点。pointcut 在程序的执行(在 AspectJ 用语中称为 joinpoint)中选取一些点,并答应 aspect 定义运行与这些 jointpoint 有关的代码。有了简单的 pointcut,我们就可以选择所有参数符合特定特征的 JNDI 查找了。但是不管我们做什么,都必须确保测试 aspect 只影响在测试代码中出现的查找。为了实现这一点,我们可以使用 cflow() pointcut。cflow 选出程序的所有在另一个 joinpoint 上下文中出现的执行点。
下面的代码片段展示了如何修改示例应用程序来使用基于 cflow 的 pointcut。
pointcut inTest() : execution(public void ClientBeanTest.test*());
/*then, later*/ cflow(inTest()) && //other conditions
这几行定义了测试上下文。第一行为 ClientBeanTest 类中什么也不返回、拥有公共访问权并以 test 一词开头的所有方法执行的集合起名为 inTest()。表达式 cflow(inTest()) 选出在这样的方法执行和其返回之间出现的所有 joinpoint。所以,cflow(inTest()) 的意思就是“当 ClientBeanTest 中的测试方法执行时”。
样本应用程序的测试组可以在两个不同的配置中构建,每一种使用不同的 aspect 。第一个配置用模拟对象替换真正的 CustomerManager。第二个配置不替换对象,但选择性地替换 ClientBean 对 EJB 组件作出的调用。在两种情况下,aspect 治理表示,同时确保客户从 CustomerManager 接收到可预知的结果。通过检查这些结果,ClientBeanTest 可以确保客户机正确使用 EJB 组件。
使用 aspect 替换 EJB 查找
第一个配置(如清单 8 所示)向示例应用程序应用了一个名为 ObjectReplacement 的 aspect。它的工作原理是替换任何对 Context.lookup(String) 方法调用的结果。
这种方法答应在 ClientBean 预期的 JNDI 配置的非就绪的环境中运行测试案例,也就是从命令行或简单的 Ant 环境运行。您可以在部署 EJB 之前(甚至在编写它们之前)执行测试案例。假如您依靠于一个超出您控制范围的远程服务,就可以不管是否能够接受在测试上下文中使用实际服务来运行单元测试了。
清单 8. ObjectReplacement aspect
import javax.naming.Context;
public aspect ObjectReplacement{
/**
* Defines a set of test methods.
*/
pointcut inTest() : execution(public void ClientBeanTest.*());
/**
* Selects calls to Context.lookup occurring within test methods.
*/
pointcut jndiLookup(String name) :
cflow(inTest()) &&
call(Object Context.lookup(String)) &&
args(name);
/**
* This advice executes *instead of* Context.lookup
*/
Object around(String name) : jndiLookup(name){
if("java:comp/env/ejb/CustomerManager".equals(name)){
return new MockCustomerManagerHome();
}
else{
throw new Error("ClientBean should not lookup any EJBs " +
"except CustomerManager");
}
}
}
pointcut jndiLookup 使用前面讨论的 pointcut 来识别对 Context.lookup() 的相关调用。我们在定义 jndiLookup pointcut 之后,就可以定义执行而不是查找的代码了。
关于“建议”
AspectJ 使用建议(advice)一词来描述在 joinpoint 执行的代码。ObjectReplacement aspect 使用一条建议(在上面以蓝色突出显示)。建议本质上讲述“当碰到 JNDI 查找时,返回模拟对象而不是继续调用方法。”一旦模拟对象返回到客户机,aspect 的工作就完成了,然后模拟对象接过控制权。MockCustomerManagerHome(作为真正的 home 对象)只从任何调用它的 create() 方法返回一个客户治理者的模拟版本。因为模拟必须实现 home 主接口,才能够合法地在正确的点进入程序,所以模拟还实现 CustomerHome 的超级接口 EJBHome 的所有的方法,如清单 9 所示。
清单 9. MockCustomerManagerHome
public class MockCustomerManagerHome implements CustomerManagerHome{
public CustomerManager create()
throws RemoteException, CreateException {
return new MockCustomerManager();
}
public javax.ejb.EJBMetaData getEJBMetaData() throws RemoteException {
throw new Error("Mock. Not implemented.");
}
//other super methods likewise
[...]
MockCustomerManager 很简单。它还为超级接口操作定义存根方法,并提供 ClientBean 使用的方法的简单实现,如清单 10 所示。
清单 10. MockCustomerManager 的模拟方法
public void register(String name) NameExistsException {
if( ! name.equals(ClientBeanTest.NEW_CUSTOMER)){
throw new NameExistsException(name + " already exists!");
}
}
public String[] getCustomersOver(int years) {
String[] customers = new String[55];
for(int i = 0; i < customers.length; i++){
customers[i] = "Customer Number " + i;
}
return customers;
}
只要模拟还在进行,这就可以列为不复杂的。成熟的模拟对象提供了答应测试轻易地定制其行为的 hook。然而,由于本示例的缘故,我尽可能地将模拟的实现保持简单。
使用 aspect 替换对 EJB 组件的调用
跳过 EJB 部署阶段可以在某种程度上减轻开发工作,但尽可能在测试达到最终目的的环境中测试代码也有好处。完全集成应用程序并运行针对部署的应用程序的测试(只替换那些对测试绝对重要的上下文部分)可以预先扫除配置问题。这是 Cactus(一个开放源代码、服务器端测试框架(请参阅“Plugging in Cactus”))背后的基本原理。
下面的示例应用程序的一个配置使用了 Cactus 来执行它在应用程序服务器中的测试。这答应测试验证 ClientManager EJB 被正确配置,并能够被容器中的其它组件访问。AspectJ 还可以将其替换能力集中在测试需要的行为上,不去理会其它组件,从而补充这种半集成的测试风格。
CallReplacement aspect 从测试上下文的相同定义开始。它接下来指定对应于 getCustomersOver() 和 register() 方法的 pointcut,如清单 11 所示:
清单 11. 选择 CustomerManager 的测试调用
public aspect CallReplacement{
pointcut inTest() : execution(public void ClientBeanTest.test*());
pointcut callToRegister(String name) :
cflow(inTest()) &&
call(void CustomerManager.register(String)) &&
args(name);
pointcut callToGetCustomersOver() :
cflow(inTest()) &&
call(String[] CustomerManager.getCustomersOver(int));
//[...]
然后 aspect 在每个相关的方法调用上定义 around 建议。当 ClientBeanTest 中出现对 getCustomersOver() 或 register() 的调用时,将改为执行相关的建议,如清单 12 所示:
清单 12. 建议替换测试中的方法调用
void around(String name) throws NameExistsException:
callToRegister(name) {
if(!name.equals(ClientBeanTest.NEW_CUSTOMER)){
throw new NameExistsException(name + " already exists!");
}
}
Object around() : callToGetCustomersOver() {
String[] customers = new String[55];
for(int i = 0; i < customers.length; i++){
customers[i] = "Customer Number " + i;
}
return customers;
}
这里的第二个配置在某种程度上简化了测试代码(请注重,对于没有实现的方法,我们不需要分开的模拟类或存根)。
可插的测试配置
AspectJ 答应您随时在这两种配置间切换。因为 aspect 可能影响不了解这两种配置的类,所以在编译时指定一组不同的 aspect 可能会导致系统在运行时和预期完全不同。样本应用程序就利用了这一点。构建替换调用和替换对象示例的两个 Ant 目标几乎完全相同,如下所示:
清单 13. 不同配置的 Ant 目标
<target name="objectReplacement" description="...">
<antcall target="compileAndRunTests">
<param name="argfile"
value="${src}/ajtest/objectReplacement.lst"/>
</antcall>
</target>
[contents of objectReplacement.lst]
@base.lst;[A reference to files included in both configurations]
MockCustomerManagerHome.java
MockCustomerManager.java
ObjectReplacement.java.
<target name="callReplacement" description="...">
<antcall target="deployAndRunTests">
<param name="argfile"
value="${src}/ajtest/callReplacement.lst"/>
</antcall>
</target>
[contents of callReplacement.lst]
@base.lst
CallReplacement.java
RunOnServer.java
Ant 脚本将 argfile 属性传送到 AspectJ 编译器。AspectJ 编译器使用该文件来决定在构建中包括哪些来源(Java 类和 aspect)。通过将 argfile 从 objectReplacement 改为 callReplacement,构建可以用一个简单的重编译改变测试策略。
插入 Cactus
示例应用程序与 Cactus 捆绑在一起提供,Cactus 是用来执行应用程序服务器中的测试的。要使用 Cactus,您的测试类必须继续 org.apache.cactus.ServletTestCase(而不是通常的 junit.framework.TestCase)。这个基类将自动与部署到应用程序服务器的测试对话。因为测试的“callReplacement”版本需要服务器,但“objectReplacement”版本不需要,所以我使用了 AspectJ 的另一种功能(叫作介绍(introdUCtion))来使测试类意识到服务器。ClientBeanTest 的源版本将继续 TestCase。假如我希望在服务器端运行测试,就可以将下面的 aspect 添加到我的构建配置中:
public aspect RunOnServer{
declare parents : ClientBeanTest extends ServletTestCase;
}
通过加入这个 aspect,我声明 ClientBeanTest 将继续 ServletTestCase,而不是 TestCase,同时将其从常规的测试案例转换为一个 Cactus 测试案例。很简洁,对吧?
要了解更多有关 Cactus 的信息,请参阅参考资料部分。
这种编译时的 aspect 插入在诸如 aspect 协助测试的情况下可能非常有好处。理想情况下,您不会希望有任何部署在生产条件中的测试代码的痕迹。有了编译时的不插入的方法,即便测试 aspect 被插入,或执行了复杂的行为修改,您还是可以很快地去掉测试部件。
结束语
为了保持较低的测试开发成本,必须单独运行单元测试。模拟对象测试通过提供被测试类依靠的代码的模拟实现隔离每个单元。但面向对象的方法无法在所属物从可全局访问的来源检索的情况下成功地替换合作代码。AspectJ 横切被测试代码结构的能力答应您“干净地”替换这类情况中的代码。
尽管 AspectJ 的确引入了一种新的编程模型(面向 aspect 的编程),本文中的方法还是很轻易把握。通过使用这些策略,您就可以编写能够成功地验证组件而不需治理系统数据的递增单元测试了。
参考资料
AspectJ
请单击文章顶部或底部的讨论参加关于本文的讨论论坛。
假如您需要了解对这个强大的语言扩展的潜力以及使用的介绍,请看看 Nicholas Lesiecki 关于 AspectJ 的第一篇文章:“Improve modularity with aspect-oriented programming”(developerWorks,2002 年 1 月)。
您可以从 www.aspectj.org 下载 AspectJ 和它的相关工具。这个站点还有关于 AOP 的常见问题解答、邮件列表、出色文档和对其它参考资料的链接。
2002 年 5 月刊的 Software Development 杂志集中讨论面向 aspect 的编程。这一期介绍了 Gregor Kiczales,并特刊了 Wes Isberg 关于用 AspectJ 进行测试的一篇文章。
单元测试工具和方法
假如您对这个流行的单元测试框架、它的众多扩展或关于单元测试的文章和论文大体上感爱好,那么 JUnit.org 就是一个很好的起点。
Nicholas Lesiecki 的 Java Tools for Extreme Programming(与 Rick Hightower 合著)一书中介绍了可以帮助您实现 XP 方法(如单元测试和连续集成)的实用工具。几乎本文中使用的所有工具(JUnit、Cactus 和 Ant)在这个出色的参考中都有所描述。
要了解更多关于模拟对象的信息,请访问 mockobjects.com,或者看看最初开始讨论它的文章“Endo Testing: Unit Testing with Mock Objects”(PDF 格式)。
假如您没有使用模拟对象,您至少应该使用 ObjectMother 模式(PDF 格式)来帮助您创建和销毁测试状态。这个对“对象之母”的介绍提供了与本文不同的另一个观察角度。
Cactus 工程使您能够轻松地测试服务器端代码。Cactus 小组计划提供一个举例说明在不久的将来如何集成 Cactus 和 AspectJ 的样本应用程序,所以请保持关注。
附加参考资料
请下载本文的示例应用程序。
示例应用程序是用 Ant 构建的,可以从 Jakarta 免费获取。
developerWorks 上有很多关于开始使用 Ant 和 JUnit 的出色文章:请参考 Erik Hatcher 的“Automating the build and test process”(2001 年 8 月)和 Malcolm Davis 的“Incremental development with Ant and JUnit”(2000 年 11 月)。
假如您希望得到更多关于 EJB 技术的背景知识来帮助您运行示例,您可以从 Trivera Technologies 下载“The Developer′s Guide to Understanding EJB 2.0。
要了解更多关于极端编程的实践和方法学的信息,请访问 XProgramming.com。
假如您的测试需要超出单元测试的范围,而达到企业级系统测试领域,请参阅 IBM Performance Management, Testing, and Scalability Services 站点,看看它能提供什么。(该站点包括一个关于企业测试的库。)
假如您在使用 VisualAge for Java 作为 IDE,那么来自 VisualAge 开发者园地的 Unit testing with VAJ 展示了 JUnit 和 VAJ 如何结合成为强大的整体,用最小的努力达到全面的代码测试。
您可以在 IBM developerWorks Java 技术专区上找到数百篇关于 Java 编程每个方面的文章。
关于作者
Nicholas Lesiecki 在互联网正值繁荣时开始接触到 Java,从那时起不断成长为 XP 和 Java 社区的突出人物。Nicholas 目前领导 eBlox Inc. 的旗舰联机目录系统 storeBlox 的开发。除了频繁地在 Tucson JUG 发表演说,他还保持 Jakarta 的 Cactus 工程的积极赞助人身份。Nick 与其它人合著了 Java Tools for Extreme Programming,一本关于在灵活的进程(如 XP)中利用开放源代码构建和测试工具的使用指南手册。Nick 希望感谢 Ron Bodkin、Wes Isberg 和 Vincent Massol 在撰写本文的过程中提供的帮助。请通过 ndlesiecki@apache.org 与 Nick 联系。
--摘自IBM网站
http://www-900.ibm.com/developerWorks/cn/java/j-aspectj2/index.sHtml