技术开发 频道

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

  AspectJ 能够在“每测试案例”的基础上提供对上下文敏感的行为修改(甚至在通常会禁止使用模仿对象的情况下)。AspectJ 的联接点模型允许名为 aspect的模块识别程序的执行点(比如从 JNDI 上下文查找对象),并定义执行这些点的代码(比如返回模仿对象,而不是继续查找)。

  aspect 通过 pointcut识别程序控制流程中的点。pointcut 在程序的执行(在 AspectJ 用语中称为 joinpoint)中选取一些点,并允许 aspect 定义运行与这些 jointpoint 有关的代码。有了简单的 pointcut,我们就可以选择所有参数符合特定特征的 JNDI 查找了。但是不管我们做什么,都必须确保测试 aspect 只影响在测试代码中出现的查找。为了实现这一点,我们可以使用 cflow() pointcut。 cflow 选出程序的所有在另一个 joinpoint 上下文中出现的执行点。

  下面的代码片段展示了如何修改示例应用程序来使用基于 cflow 的 pointcut。 

1 pointcut inTest() : execution(public void ClientBeanTest.test*());
2 /*then, later*/ cflow(inTest()) && //other conditions
3

        这几行定义了测试上下文。第一行为 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

1 import javax.naming.Context;
2 public aspect ObjectReplacement{
3        /**
4         * Defines a set of test methods.
5         */
6        pointcut inTest() : execution(public void ClientBeanTest.*());
7        /**
8         * Selects calls to Context.lookup occurring within test methods.
9         */
10        pointcut jndiLookup(String name) :
11                 cflow(inTest()) &&
12                 call(Object Context.lookup(String)) &&
13                 args(name);
14                 
15        /**
16         * This advice executes *instead of* Context.lookup
17         */
18        Object around(String name) : jndiLookup(name){
19            if("java:comp/env/ejb/CustomerManager".equals(name)){
20                return new MockCustomerManagerHome();
21            }
22            else{
23                throw new Error("ClientBean should not lookup any EJBs " +
24                                "except CustomerManager");
25            }
26        }
27 }
28

        pointcut jndiLookup 使用前面讨论的 pointcut 来识别对 Context.lookup() 的相关调用。我们在定义 jndiLookup pointcut 之后,就可以定义执行而不是查找的代码了。

  关于“建议”

  AspectJ 使用 建议(advice)一词来描述在 joinpoint 执行的代码。 ObjectReplacement aspect 使用一条建议(在上面以蓝色突出显示)。建议本质上讲述“当遇到 JNDI 查找时,返回模仿对象而不是继续调用方法。”一旦模仿对象返回到客户机,aspect 的工作就完成了,然后模仿对象接过控制权。 MockCustomerManagerHome (作为真正的 home 对象)只从任何调用它的 create() 方法返回一个客户管理者的模仿版本。因为模仿必须实现 home 主接口,才能够合法地在正确的点进入程序,所以模仿还实现 CustomerHome 的超级接口 EJBHome 的所有的方法,如清单 9 所示。

  清单 9. MockCustomerManagerHome

1 public class MockCustomerManagerHome implements CustomerManagerHome{
2        public CustomerManager create()
3          throws RemoteException, CreateException {
4            return new MockCustomerManager();
5        }
6        public javax.ejb.EJBMetaData getEJBMetaData() throws RemoteException {
7            throw new Error("Mock. Not implemented.");
8        }
9 //other super methods likewise
10 [...]
11

  MockCustomerManager

  很简单。它还为超级接口操作定义存根方法,并提供 ClientBean 使用的方法的简单实现,如清单 10 所示。

  清单 10. MockCustomerManager 的模仿方法

1 public void register(String name) NameExistsException {
2      if( ! name.equals(ClientBeanTest.NEW_CUSTOMER)){
3          throw new NameExistsException(name + " already exists!");
4      }
5 }
6 public String[] getCustomersOver(int years) {
7      String[] customers = new String[55];
8      for(int i = 0; i < customers.length; i++){
9          customers[i] = "Customer Number " + i;
10      }
11      return customers;
12 }
13

  只要模仿还在进行,这就可以列为不复杂的。成熟的模仿对象提供了允许测试轻易地定制其行为的 hook。然而,由于本示例的缘故,我尽可能地将模仿的实现保持简单。

  使用 aspect 替换对 EJB 组件的调用

  跳过 EJB 部署阶段可以在某种程度上减轻开发工作,但尽可能在测试达到最终目的的环境中测试代码也有好处。完全集成应用程序并运行针对部署的应用程序的测试(只替换那些对测试绝对重要的上下文部分)可以预先扫除配置问题。这是 Cactus(一个开放源代码、服务器端测试框架背后的基本原理。

  下面的示例应用程序的一个配置使用了 Cactus 来执行它在应用程序服务器中的测试。这允许测试验证 ClientManager 被正确配置,并能够被容器中的其它组件访问。AspectJ 还可以将其替换能力集中在测试需要的行为上,不去理会其它组件,从而补充这种半集成的测试风格。

  CallReplacement aspect 从测试上下文的相同定义开始。它接下来指定对应于 getCustomersOver() 和 register() 方法的 pointcut,如清单 11 所示:

  清单 11. 选择 CustomerManager 的测试调用

1 public aspect CallReplacement{
2        pointcut inTest() : execution(public void ClientBeanTest.test*());
3        pointcut callToRegister(String name) :
4                        cflow(inTest()) &&
5                        call(void CustomerManager.register(String)) &&
6                        args(name);
7        pointcut callToGetCustomersOver() :
8                        cflow(inTest()) &&
9                        call(String[] CustomerManager.getCustomersOver(int));
10        //[...]
11

  然后 aspect 在每个相关的方法调用上定义 around 建议。当 ClientBeanTest 中出现对 getCustomersOver() 或 register() 的调用时,将改为执行相关的建议,如清单 12 所示:

  清单 12. 建议替换测试中的方法调用

1 void around(String name) throws NameExistsException:
2 callToRegister(name) {
3            if(!name.equals(ClientBeanTest.NEW_CUSTOMER)){
4                throw new NameExistsException(name + " already exists!");
5            }
6        }
7        Object around() : callToGetCustomersOver() {
8            String[] customers = new String[55];
9            for(int i = 0; i < customers.length; i++){
10                customers[i] = "Customer Number " + i;
11            }
12            return customers;
13        }
14

  这里的第二个配置在某种程度上简化了测试代码(请注意,对于没有实现的方法,我们不需要分开的模仿类或存根)。

  可插的测试配置

  AspectJ 允许您随时在这两种配置间切换。因为 aspect 可能影响不了解这两种配置的类,所以在编译时指定一组不同的 aspect 可能会导致系统在运行时和预期完全不同。样本应用程序就利用了这一点。构建替换调用和替换对象示例的两个 Ant 目标几乎完全相同,如下所示:

  清单 13. 不同配置的 Ant 目标

1 <target name="objectReplacement" description="...">
2          <antcall target="compileAndRunTests">
3            <param name="argfile"
4                         value="${src}/ajtest/objectReplacement.lst"/>
5          </antcall>
6        </target>
7        [contents of objectReplacement.lst]
8        @base.lst;[A reference to files included in both configurations]
9        MockCustomerManagerHome.java
10        MockCustomerManager.java
11        ObjectReplacement.java.
12        <target name="callReplacement" description="...">
13          <antcall target="deployAndRunTests">
14            <param name="argfile"
15                         value="${src}/ajtest/callReplacement.lst"/>
16          </antcall>
17        </target>
18        [contents of callReplacement.lst]
19        @base.lst
20        CallReplacement.java
21        RunOnServer.java
22

Ant 脚本将 argfile 属性传送到 AspectJ 编译器。AspectJ 编译器使用该文件来决定在构建中包括哪些来源(Java 类和 aspect)。通过将 argfile 从 objectReplacement 改为 callReplacement ,构建可以用一个简单的重编译改变测试策略。

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

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

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

0
相关文章