AspectJ和模仿对象的测试灵活性

用“test-only”行为增强单元测试

简介: 在开发过程中结合了单元测试的程序员都了解这样做带来的好处:代码更简洁,敢于重构、速度更快。但即便是最执著的单元测试者,在碰到测试行为依赖于系统状态的类的情况时,也会显得信心不足。Nicholas Lesiecki 是一名受人尊敬的 Java 程序员,也是 XP 社区的领导者,他将介绍围绕测试案例隔离的问题,并向我们展示如何使用模仿对象(mock object)和 AspectJ 来开发精确和健壮的单元测试。

最近,人们对极端编程(Extreme Programming,XP)的关注已经扩大到它的一个最具可移植性的应用上:单元测试和最初测试设计。因为软件工作室已经开始采用 XP 的开发方法,我们可以看到,因为有了一套全面的单元测试工具,很多开发者的开发质量和速度都得到了提高。但编写好的单元测试耗时费力。因为每个单元都与其它单元合作,所以编写单元测试可能需要大量的设置代码。这使得测试变得更加昂贵,而且在特定情况下(比如代码充当远程系统的客户机时),这样的测试可能几乎无法实现。

在 XP 中,单元测试弥补了集成测试和验收测试的不足。后两种测试类型可能由独立的小组进行,或者作为独立的活动进行。但是单元测试是与要测试的代码同时编写的。面对日益逼近的截止期限和令人头痛的单元测试带来的压力,我们很可能随便编写一个测试了事,或者完全放弃测试。因为 XP 依赖于积极的动机和自给自足的习惯,所以 XP 过程(和项目!)的最佳利益就是使测试保持集中和易于编写。

所需背景

本文的重点是 AspectJ 的单元测试,所以文章假定您熟悉基本的单元测试方法。如果您不熟悉 AspectJ,那么在继续之前阅读一下我对 AspectJ 的介绍很可能会对您有所帮助(请参阅 参考资料)。这里所说的 AspectJ 方法不是非常复杂,但面向 aspect 的编程却需要一点时间去习惯。为了运行示例,您需要在测试机器上安装 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 bj =           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 EJBNumber1public 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(一个开放源代码、服务器端测试框架)背后的基本原理。

下面的示例应用程序的一个配置使用了 Cactus 来执行它在应用程序服务器中的测试。这允许测试验证 ClientManager 被正确配置,并能够被容器中的其它组件访问。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 目标

                             [contents of objectReplacement.lst]     @base.lst;[A reference to files included in both configurations]     MockCustomerManagerHome.java     MockCustomerManager.java     ObjectReplacement.java.                                  [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 测试案例。很简洁,对吧?

这种编译时的 aspect 插入在诸如 aspect 协助测试的情况下可能非常有好处。理想情况下,您不会希望有任何部署在生产条件中的测试代码的痕迹。有了编译时的不插入的方法,即便测试 aspect 被插入,或执行了复杂的行为修改,您还是可以很快地去掉测试部件。

结束语

为了保持较低的测试开发成本,必须单独运行单元测试。模仿对象测试通过提供被测试类依赖的代码的模仿实现隔离每个单元。但面向对象的方法无法在所属物从可全局访问的来源检索的情况下成功地替换合作代码。AspectJ 横切被测试代码结构的能力允许您“干净地”替换这类情况中的代码。

尽管 AspectJ 的确引入了一种新的编程模型(面向 aspect 的编程),本文中的方法还是很容易掌握。通过使用这些策略,您就可以编写能够成功地验证组件而不需管理系统数据的递增单元测试了。

爱的力量大到可以使人忘记一切,却又小到连一粒嫉妒的沙石也不能容纳

AspectJ和模仿对象的测试灵活性

相关文章:

你感兴趣的文章:

标签云: