Author: 金 明

About 金 明

ThoughtWorks 高级咨询师 InfoQ主编 SCJP, 系统分析师 Consultant@ThoughtWorks, Editor@InfoQ, SCJP, Certified System Analyst. Focus on Agile, PM, OO, Arichtecture, and Open Source

由某手机厂商现状漫谈敏捷

跟同事聊天,他原先是在一著名手机厂商研发中心工作,主要是做该厂商手持终端设备上的系统软件,于是自然聊到“摩托,也要骡拉”上来。近几年该厂的发展很不景气,好几年也没见一款拿得出手的手机,在中国的市场占有率从前三降到排名之外,连在国贸的冠名大厦都卖掉了。同事说起来也是颇多无奈,讲述了他看到的情况。

据他观察,该公司内部是出现了这个几个问题:
1. 基础平台不稳定,大量功能被任意加到平台里面,导致越来越复杂,后期维护扩展完全不可能
2. 产品设计部的设计到产品研发,中间经历太长时间,不能响应市场需求
3. 产品研发到最后才发现功能缺陷或者性能缺陷,最后只能 cancel

这些问题的产生原因相信见仁见智。撇开管理层和多部门间合作的问题,个人觉得这是传统软件开发模式下出现的典型问题,特别是基于瀑布模式的软件开发。不能很快地响应变化,前期环节很难得到后面环节的反馈。由于开发模型是线性的,只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险;早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果。这样就会导致很多启动初期信心满满都看好的项目最后只能前赴后继地陷入“焦泥坑”。实际过程中会加入更多的开发人员,使用更多的先进开发工具试图解决问题,但对于开发问题的解决,这些都是作用不大的,甚至是帮倒忙的。Brooks’ law 告诉我们,“Adding manpower to a late software project makes it later.”

“开发过程中变数太多了!”同事感慨到,于是又说到敏捷方法的拥抱变化。其实敏捷何尝能减少变化。软件开发的过程就是将问题域映射到软件系统,然后提供软件层面的解决方案。这里面天然存在两个“再创造”的过程:问题域的分析建模,软件的实现运行。任何一个环节的复杂性都会被放大累积进整个过程的复杂性,那么有没有一劳永逸的办法来解决这两个问题?同样是 Fred Brooks 告诉我们 “there is no Silver Bullet.”

软件开发的复杂性可以分为两种:本质复杂性和附加复杂性。其中附加复杂性包括人的复杂、工具技术的复杂,外部的复杂等。这些附加复杂性都是希望被限制到最小限度,可能造成的影响被限制在最小范围内。这也是各种软件开发方法试图解决的主要问题。至于本质复杂性,主要是问题域本身的业务复杂,这是社会、组织,甚至各种因素造成的不可逃避的问题,是任何软件方法都不可能抹掉的。因此,如何减少附加复杂性,尽可能解决本质复杂性,就是软件开发方法的使命,也是判断软件方法是否有效的唯一标准。可悲的是,传统的软件开发大多是着眼于通过增加附加复杂性来解决本质复杂性,或者通过文档、或者通过层层审批、或者通过freeze code base等等,但最后都被证明是刻舟求剑、缘木求鱼。

与传统方法不同,敏捷方法就是试图解决软件开发过程中的附加复杂性,把对解决本质复杂性的关注重新放到舞台的中央,并提供应对本质复杂性的足够可能。对于解决附加复杂性,敏捷宣言有“可工作的软件胜于详尽冗繁的文档”,也有很多相关的实践来保持对附加复杂性的不侵入,就不赘述了。那么敏捷是如何拥抱本质复杂性呢?那就是保持简单和客户 involved。

简单,于是可以足够轻量来调整原来的实现;简单,于是团队内部容易达到领域知识共享;简单,于是开发过程透明性大大增强……这一切的结果都指向“响应变化”。user case 简单了,就很容易来进行确认,包括前期和客户的需求确认,也包括后期开发结果的确认。代码简单了,就很容易进行重构,增进设计,逐步兼容添加问题域中的复杂性。开发计划简单了,现在不用关心几个月后的事情,只需要关注到下一个迭代和当前 release 涉及的需求。“简约,而不简单”,大家都轻松了,有时间培养自己的业余兴趣了。

这是从开发团队的角度来看到响应变化。客户 involved 就使得这些变化能被客户感知和认同,让客户尽可能主动思考现实问题域中的复杂性是否有改进的地方,规避了可能的风险,也有利于培养出长期积极的合作关系。这是一个很良性的互动过程,也是一个逐步走向双赢的过程。这也是项目管理层和公司决策层会喜欢看到的结果。

“这些都很美好,但执行起来还得看人”,同事又抛出了这样的论点。我默然,世界上最复杂的莫过于人了。不管方法理论上多么完美,实践起来多么容易,只有真正有合适的人,让合适的人去做合适的事,才能不致于明珠暗投,徒然神伤了。呜呼

DIP 沉思录

Dependency Injection 这个名词,是在 Martin Fowler 的《Inversion of Control Containers and the Dependency Injection pattern》文章之后才广为人知。在文章中,Martin 解释了当时初起流行的 IOC 概念:为了消除应用程序对插件实现的依赖,程序的主控权从应用程序移到了框架。为了让 IOC 概念不那么令人迷惑,Martin 把流行的几种 IOC Container 实现模式命名为 Dependency Injection Pattern(DIP,下文简称 DI 模式)。很明显,DI 的定义更准确形象,而且因为 Martin 在软件开发社区巨大的影响力,DI 模式以及 spring framework 作为其最成功的实现之一很快被软件开发社区接受并成为日常开发的必备利器。

“依赖注入”给我们代码的编写,特别是测试代码的编写带来了很多好处。借助于 DI 模式,我们可以将服务的依赖声明和具体实现相分离,通过配置不同的服务实例交给框架管理,来让应用程序获得良好的灵活性和柔性。

比如我们有这样的代码:

public void hello() {
Foo foo
= new Foo();
foo.sayHello();
//
}

这种情况下,hello() 方法就依赖于 Foo 类的 sayHello() 方法实现,会给测试带来了很多的不稳定性,比如耗时、服务异常之类。如果依照 DI 模式来重构,这段代码就会变成这样:

public void hello(Foo foo) {
foo.sayHello();
//
}

在测试时我们就可以使用行为确定符合预期的‘mock’的 Foo 类来代替实际的 Foo 类,从而将测试关注点聚集到 hello() 方法,也避免了 Foo 类 sayHello() 方法的具体实现对测试可能带来的影响。

以上就是 DIP 最常见也是最令人熟悉的一层涵义,从 IOC、spring framework 开始接受 DIP,自然而然会把 DIP 等同于 DI 模式,但其实它还有另一层涵义。在《Agile Software Developement:principles,Patterns,and Practices》的第11章 依赖倒置原则,给出了 DIP 的另一种解读:DIP —— 依赖倒置原则,这里的 DIP 就不再是 Dependency Injection Pattern(依赖注入模式),而是 Dependency Inversion Principle(依赖倒置原则,以下简称 DI 原则)。DI 原则主要包括下面两条启发式规则:
A. 高层模块不应依赖于低层模块。二者都应依赖于抽象
B. 抽象不应依赖于细节。细节应依赖于抽象

人们通常会用好莱坞法则来诠释启发规则 A:“Don’t call us, we’ll call you.”其中的 we/us,就是指高层模块,you 则是低层模块,高层模块来决定低层的模块,就像好莱坞制片人最终掌握着演员上镜与否的生杀大权。

我们来看看软件系统中常见的类关系,高层的 Policy Layer 使用了低层的 Mechanism Layer,而低层的 Mechanism Layer 又使用了更低层的 Utility Layer(见图1)

图 1

图1中的依赖关系是传递的:高层的 Policy Layer 对于其下一直到 Utility Layer 的改动都是敏感的,一旦我们修改了低层模块的实现,高层模块不得不也修改相应的实现来适应低层模块的修改。这种依赖关系,与软件复用的目标是相悖的,而且也不符合实际的业务变化。我们通常会需要切换低层的实现方式或者版本,而很少会去修改高层的业务。比如有一个证书申请发放系统,需要使用异步的消息机制来处理用户请求。客户可能会要求把底层的 MessageQueue 从 IIS 切换成 ActiveQ,但高层的证书申请发放的业务流程是稳定的,不会因低层基础服务的改变而作出修改。

所以,为了应付现实中的变化,也为了向高层屏蔽这些变化,我们通常会给低层模块抽象出接口,作为高层和低层之间的契约。这样,高层模块就应该只与低层模块的接口打交道,不再关心低层模块的实现细节。而低层模块的实现,我们可以通过配置文件由框架来切换,不影响到高层的行为。说到这里,大家应该可以看到 Spring 的影子了。此时类关系图就变成了下图(图2)

图 2

嗯,现在的类关系已经遵循接口依赖了,甚至加上了框架的 DI 模式,系统也具有了一定的灵活性和易改变性,看起来很像大多数的系统了。但是,这是不是就是符合 DI 原则呢?我们可以看到,这里的接口通常是由低层模块来定义和派生,也就是低层模块抽象出来提供给外界调用的服务接口。高层模块其实还是依赖于低层模块,只是这次高层模块产生依赖的是低层模块里面声明的接口。想起曾经在 javaeye 上看到一篇帖子,作者抱怨为什么 java 不提供 extracts 关键字,这样就可以由框架或者容器在具体类上抽出接口定义(与 implements 对应),省得手工创建这些接口。或许这是目前依赖注入框架带来的误区吧:使用人员不理解 DIP 的本意,单纯是为框架的约束而创建。此时声明的类关系显然还是没有完全体现 DI 原则的精髓。

在书中 Robert 进一步对 DI 原则给出了解释:“请注意这里的倒置不仅仅是依赖关系的倒置,也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口,但是应用 DIP (DI 原则)时,我们发现往往是消费者拥有抽象接口,而它们的服务者则从这些接口派生。”基于这种思路,前面例子更符合面向对象思想的层次关系图如下(图3)
图 3

图3和图2的区别主要在接口的所有权:高层模块应该拥有接口所有权,低层模块派生自高层模块里定义的接口。这就意味着:
1. 高层模块引用的接口定义应该和高层模块的其他类放在一起
2. 高层模块的复用是把高层拥有的所有类和接口定义作为一个整体来复用的
3. 接口定义的改变只有根据高层模块的需要才进行的,而不是低层模块

OK,直到现在,我们才能说 DI 原则原来是这个意思,否则,体会不到就有可能误人误己。Robert 在本章的结论中说“事实上,这种依赖关系的倒置正是好的面向对象设计的标志所在,使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。如果程序的依赖关系不是倒置的,它就是过程化的设计。”信哉此言!

那么,要按照 OOA & OOD 的设计思路来进行系统设计,DI 原则对我们有什么帮助呢?其实,DI 原则不仅仅是抽象的原则,而且是可以启发推导出出种种具体的实践。我们来看看对 OOA & OOD 的帮助。因为依赖关系是倒置的,就可以通过对高层策略的抽象和定义驱动出低层服务者的接口。以此类推,直到把最底层的模块设计出为止。那什么是高层策略呢?怎么找出潜在的抽象?书中同样给出了答案:“它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统——它是隐喻(metaphore)”更有详细的具体实践,敬请关注本博其他文章。

References:
Inversion of Control Containers and the Dependency Injection pattern,http://www.martinfowler.com/articles/injection.html,Martin Fowler
控制反转与依赖注入模式,http://gigix.blogdriver.com/diary/gigix/inc/DependencyInjection.pdf,熊节 译
《Agile Software Developement:principles,Patterns,and Practices》,Robert Fowler 著,邓辉 译,孟岩 审

Spring2.5 访问 Session 属性的四种策略

WEB 应用通常会引入 Session,用来在服务端和客户端之间保存一系列动作/消息的状态,比如网上购物维护 user 登录信息直到 user 退出。在 user 登录后,Session 周期里有很多 action 都需要从 Session 中得到 user,再验证身份权限,或者进行其他的操作。这其中就会涉及到程序去访问 Session属性的问题。在java中,Servlet 规范提供了 HttpSession对象来满足这种需求。开发人员可以从 HttpServletRquest对象得到 HttpSession,再从HttpSession中得到状态信息。

还是回到购物车的例子,假设在 controller 某个方法(本文简称为action)中我们要从HttpSession中取到user对象。如果基于Servlet,标准的代码会是这样的:

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 User user 
= (User)req.getSession().getAttribute(currentUser);
 
//
}

这样的代码在传统的Servlet程序中是很常见的:因为使用了 Servlet API,从而对 Servlet API产生依赖。这样如果我们要测试 action,我们就必须针对 HttpServletRequest、HttpServletResponse 和 HttpSession类提供 mock 或者 stub 实现。当然现在已经有很多开源的 Servlet 测试框架帮助我们减轻这个痛苦,包括 Spring 就自带了对了这些类的 stub 实现,但那还是太冗繁琐碎了。那有没有比较好的办法来让我们的 controller 更 POJO,让我们的 action 脱离 Servlet API 依赖,更有益于测试和复用呢?我们来看看在 Spring2.5 中访问 Session 属性的几种策略,并将在本博的后续文章继续探究解决方案选择后面的深层含义。

(一)通过方法参数传入HttpServletRequest对象或者HttpSession对象
笔者的前一篇文章已经简单介绍了Spring2.5的annotation使得 controller 摆脱了 Servlet API 对方法参数的限制,这里就不赘述了。有兴趣的同学可以参考这里。Spring对annotationed的 action 的参数提供自动绑定支持的参数类型包括 Servlet API 里面的 Request/Response/HttpSession(包含Request、Response在Servlet API 中声明的具体子类)。于是开发人员可以通过在 action 参数中声明 Request 对象或者 HttpSession 对象,来让容器注入相应的对象。

action 的代码如下:

@RequestMapping
public void hello(HttpSession session){
 User user 
= (User)session.getAttribute(currentUser);
 
//
}

优点:
1. 程序中直接得到底层的 Request/HttpSession 对象,直接使用 Servlet API 规范中定义的方法操作这些对象中的属性,直接而简单。
2. action 需要访问哪些具体的 Session 属性,是由自己控制的,真正精确到 Session 中的每个特定属性。
不足:
1. 程序对 Servlet API 产生依赖。虽然 controller 类已经不需要从 HttpServlet 继承,但仍需要 Servlet API 才能完成编译运行,乃至测试。
2. 暴露了底层 Servlet API,暴露了很多并不需要的底层方法和类,开发人员容易滥用这些 API。

(二)通过定制拦截器(Interceptor)在controller类级别注入需要的User对象
Interceptor 是 Spring 提供的扩展点之一,SpringMVC 会在 handle 某个 request 前后调用在配置中定义的 Interceptor 完成一些切面的工作,比如验证用户权限、处理分发等,类似于 AOP。那么,我们可以提取这样一个“横切点”,在 SpringMVC 调用 action 前,在 Interceptor 的 preHandle 方法中给 controller 注入 User 成员变量,使之具有当前登录的 User 对象。

此外还需要给这些特定 controller 声明一类 interface,比如 IUserAware。这样开发人员就可以只针对这些需要注入 User 对象的 controller 进行注入增强。

IUserAware 的代码:

public interface IUserAware {
 
public void setUser();
}

controller 的代码:

@Controller
public GreetingController implements IUserAware {
 
private User user;
 
public void setUser(User user){
  
this.user = user;
 }

 
 @RequestMapping
 
public void hello(){
  
//user.sayHello();
 }

 
//
}

Interceptor 的代码:

public class UserInjectInterceptor extends HandlerInterceptorAdapter {
 @Override
    
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        
if (handler.isAssignableFrom(IUserAware)){
         User user 
= (User)httpServletRequest.getSession().getAttribute(currentUser);
         IUserAware userAware 
= (IUserAware) handler;
         userAware.setUser(user);
        }

        
return super.preHandle(httpServletRequest, httpServletResponse, handler);
    }

    
//
}

为了让 SpringMVC 能调用我们定义的 Interceptor,我们还需要在 SpringMVC 配置文件中声明该 Interceptor,比如:

<bean class=”org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping”>
    
<property name=”interceptors”>
        
<list>
            
<ref bean=”userInjectInterceptor”/><!– userInjectInterceptor bean 的声明省略–>
        
</list>
    
</property>
</bean>

优点:
1. 对 Servlet API 的访问被移到了自 SpringMVC API 扩展的 Interceptor,controller 不需要关心 User 如何得到。
2. 开发人员可以通过随时添加或移除 Interceptor 来完成对不同参数在某一类型 controller 上的注入。
3. controller 的 User 对象通过外界注入,测试时开发人员可以很容易地注入自己想要的 User 对象。
4. controller 类去掉了对 Servlet API 的依赖,更 POJO 和通用。
5. controller 类是通过对 interface 的声明来辅助完成注入的,并不存在任何继承依赖。
不足:
1. SpringMVC 对 controller 默认是按照单例(singleton)处理的,在 controller 类中添加一个成员变量,可能会引起多线程的安全问题。
2. 因为 User 对象是定义为 controller 的成员变量,而且是通过 setter 注入进来,在测试时需要很小心地保证对controller 注入了 User 对象,否则有可能我们拿到的就不一定是一个“好公民”(Good Citizen)。

其实,一言而蔽之,这些不足之所以出现,是因为我们把某个 action 级别需要的 User 对象上提到 controller 级别,破坏了 the convention of stateless for controller classes,而 setter 方式的注入又带来了一些隐含的繁琐和不足。当然,我们可以通过把 controller 声明为“prototype”来绕过 stateless 的约定,也可以保证每次 new 一个 controller 的同时给其注入一个 User 对象。但是我们有没有更简单更 OO 的方式来实现呢?答案是有的。

(三)通过方法参数处理类(MethodArgumentResolver)在方法级别注入User对象
正如前面所看到的,SpringMVC 提供了不少扩展点给开发人员扩展,让开发人员可以按需索取,plugin 上自定义的类或 handler。那么,在 controller 类的层次上,SpringMVC 提供了 Interceptor 扩展,在 action 上有没有提供相应的 handler 呢?如果我们能够对 action 实现注入,出现的种种不足了。

通过查阅 SpringMVC API 文档,SpringMVC 其实也为 action 级别提供了方法参数注入的 Resolver 扩展,允许开发人员给 HandlerMapper 类 set 自定义的 MethodArgumentResolver。

action 的代码如下:

@RequestMapping
public void hello(User user){
 
//user.sayHello()
}

Resolver 的代码如下:

public class UserArgumentResolver implements WebArgumentResolver {

    
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
        
if (methodParameter.getParameterType().equals(User.class)) {
            
return webRequest.getAttribute(currentUser, RequestAttributes.SCOPE_SESSION);
        }

        
return UNRESOLVED;
    }

}


配置文件的相关配置如下:

<bean class=”org.springframework.web.servlet.mvc.annotation.OwnAnnotationMethodHandlerAdapter”>
    
<property name=”customArgumentResolver”>
        
<ref bean=”userArgumentResolver”/><!– userArgumentResolver bean 的定义省略 –>
    
</property>
</bean>

优点:
1. 具备第二种方案的所有优点
2. 真正做到了按需分配,只在真正需要对象的位置注入具体的对象,减少其他地方对该对象的依赖。
3. 其他人能很容易地从 action 的参数列表得知 action 所需要的依赖,API 更清晰易懂。
4. 对于很多 action 需要的某一类参数,可以在唯一的设置点用很方便一致的方式进行注入。
不足:
1. 对象依赖注入是针对所有 action, 注入粒度还是较粗。不能做到具体 action 访问具体的 Session 属性

(四)通过 SpringMVC 的 SessionAttributes Annotation 关联 User 属性
SpringMVC 文档提到了 @SessionAttributes annotation,和 @ModelAttribute 配合使用可以往 Session 中存或者从 Session 中取指定属性名的具体对象。文档里说;

The type-level @SessionAttributes annotation declares session attributes used by a specific handler. This will typically list the names of model attributes which should be transparently stored in the session or some conversational storage, serving as form-backing beans between subsequent requests.

很明显,@SessionAttributes 是用来在 controller 内部共享 model 属性的。从文档自带的例子来看,标注成 @SessionAttributes 属性的对象,会一直保留在 Session 或者其他会话存储中,直到 SessionStatus 被显式 setComplete()。那这个 annotation 对我们有什么帮助呢?

答案就是我们可以在需要访问 Session 属性的 controller 上加上 @SessionAttributes,然后在 action 需要的 User 参数上加上 @ModelAttribute,并保证两者的属性名称一致。SpringMVC 就会自动将 @SessionAttributes 定义的属性注入到 ModelMap 对象,在 setup action 的参数列表时,去 ModelMap 中取到这样的对象,再添加到参数列表。只要我们不去调用 SessionStatus 的 setComplete() 方法,这个对象就会一直保留在 Session 中,从而实现 Session 信息的共享。

controller的代码如下:

@Controller
@SessionAttributes(
currentUser)
public class GreetingController{
 @RequestMapping
 
public void hello(@ModelAttribute(currentUser) User user){
  
//user.sayHello()
 }

 
//
}

使用这种方案,还需要在 SpringMVC 配置文件的 ViewResolver 定义处,加上 p:allowSessionOverride=”true”,这样如果你对 User 对象做了修改,SpringMVC 就会在渲染 View 的同时覆写 Session 中的相关属性。

优点:
1. 具备第二种方案的所有优点
2. 使用 Annotation 声明对 Session 特定属性的存取,每个 action 只需要声明自己想要的 Session 属性。
3. 其他人能很容易地从 action 的参数列表得知 action 所需要的依赖,API 更清晰易懂。
不足:
1. 对于相同属性的 Session 对象,需要在每个 action 上定义。
2. 这种方案并不是 SpringMVC 的初衷,因此有可能会引起一些争议。

纵观这四类方法,我们可以看出我们对 Session 属性的访问控制设置,是从所有 Servlet,到某一类型的 controller 的成员变量,到所有 action 的某一类型参数,再到具体 action 的具体对象。每种方案都有各自的优点和不足:第一种方案虽然精确,但可惜引入了对 Servlet API 的依赖,不利于 controller 的测试和逻辑复用。第二、三种方案虽然解决了对 Servlet API 的依赖,也分别在 controller 和 action 级别上提供了对 Session 属性的访问,但注入粒度在一定程度上还是不够细,要想对具体属性进行访问可能会比较繁琐。不过,这在另一方面也提供了简便而统一的方法来对一系列相同类型的参数进行注入。第四种方案通过使用 Annotation,不仅摆脱了 Servlet API 的依赖,而且在 action 级别上提供了对 Session 具体属性的访问控制。但是这种访问有可能会粒度过细,需要在很多不同 action 上声明相同的 annotation。而且,毕竟这种用法并不是 SpringMVC 的初衷和推荐的,可能会带来一些争议。

本文演示了 Spring2.5 访问 Session 属性的几种不同解决方案,并分析了各自的优点和不足。本文并不打算对这些解决方案评出对错,只是试图列出在选择方案时的思维过程以及选择标准。每种方案都能满足某一类上下文的需求,在特定的开发环境和团队中都可能会是最优的选择。但是笔者还是发现,整个过程中,一些平常容易忽视的 OOP 的准则或者原则在发挥着效应,鉴于本文篇幅已经较长,就留到后续文章中继续探讨解决方案选择背后的深层含义,敬请期待。

利用 Spring2.5 和 Reflection 简化测试中的mock

spring2.5最大的特色就是全面使用annotation代替xml配置,包括IOC Container、springMVC和 TestContext测试框架等,给我们开发带来了极大的便利。springMVC的新特性在这篇文章里面已经有了比较详尽的介绍,而对于spring的新TestContext测试框架,大家也可以从这里得到详细的例子说明,有兴趣的可以去仔细阅读,本文不再赘述。总而言之,通过spring2.5提供的 annotation,我们可以让我们的类——包括controller,Test等职责特殊的类——更 POJO 化,更易于测试,也提高了 TestCase的开发效率。

在开发过程中,我们通常需要mock特定的对象来测试预期行为,或者使用stub对象来提高单元测试效率。最常见的例子就是在多层webapp中,在controller类的测试方法里mock或 stub底层dao类的方法,从而减轻单元测试时数据库操作的开销,加快单元测试速率。至于Reflection,已不是java的新概念了,各样框架基本上都有使用Reflection来增强Runtime的动态性。而java5里Reflection效率的提升和annotation的引入,更是极大地提高java语言的动态性,让开发人员得到更多Runtime的灵活性。本文将演示如何使用spring2.5和Reflection简化测试中的 mock,使用的JUnit框架是JUnit4.4,mock框架是Easymock2.4。

让我们先看看最原始的使用mock对象的测试(假设基于jdk5进行开发,使用了包括static import,varargs等新特性):

import static org.easymock.EasyMock.*;

public void HelloworldTest extends AbstractSingleSpringContextTests {
    
private Foo foo = createMock(Foo.class);
    
private Bar bar = createMock(Bar.class);
    
private Helloworld helloworld;
    
    @Before
    
public void before() {
        reset(foo, bar);
        helloworld 
= new Helloworld(foo, bar);
    }
    
    @After
    
public void after() {
        verify(foo, bar);
    }
    
    @Test
    
public void shouldSayHello() {
        
//set expectations about foo/bar
        replay(foo, bar);
        
        helloworld.sayHello();
        
//assert verification
    }
    
    
//
}

可以看到,因为使用了 Spring 老版本的 TestContext,上面的代码至少有两个方面是需要加强的:
1. 需要大量的 mock 对象创建操作,与真正的 Test Case 无关的繁琐代码,而且还引入了对Spring Context Test 类的继承依赖
2. 针对不同的 Test 类,因为用到不同的 mock 对象,每次都需要显式去指明 reset/replay/verify 用到的 mock 对象

针对上面的两个问题,我们有相应的解决方案来改进:
1. 使用spring来替我们创建mock对象,由spring IOC Container在runtime注入需要的mock对象
2. 提供更通用的rest/replay/verify机制来验证mock对象,而不是每个 Test 类都需要单独处理

1. 每个mock对象都需要手工创建么?答案当然是否定的,我们有FactoryBean。通过在配置文件中指定bean的定义,让spring来替我们创建mock对象。如下是针对Foo类的定义:

<bean id=”mockFoo” class=”org.easymock.EasyMock” factory-method=”createMock”>
    
<constructor-arg index=”0″ value=”Foo”/>
</bean>

< /constructor-arg>与此同时,Spring TestContext框架提供了 @ContextConfiguration annotation 允许开发人员手工指定 Spring 配置文件所在的位置。这样,开发过程中,如果开发人员遵循比较好的配置文件组织结构,可以维护一套只用于测试的对象关系配置,里面只维护测试用到的 mock 对象,以及测试中用到的对 mock 对象有依赖关系的对象。在产品代码中则使用另一套配置文件,配置真实的业务对象。

JUnit4.4 之后,Test 类上可以通过 @RunWith 注解指定测试用例的 TestRunner ,Spring TestContext框架提供了扩展于 org.junit.internal.runners.JUnit4ClassRunner 的 SpringJUnit4ClassRunner,它负责总装 Spring TestContext 测试框架并将其统一到 JUnit 4.4 框架中。这样,你可以把 Test 类上的关于 Spring Test 类的继承关系去掉,并且使用 JUnit4 之后引入的 annotation 去掉其他任何 JUnit3.8 需要的约定和方法继承,让 Test 类更加 POJO。

Test 类也是“纯正” 的 java 对象,自然也可以通过 Spring 来管理依赖关系:在 Test 类的成员变量上加上 @Autowired 声明,使用 SpringJUnit4ClassRunner 运行 Test Case。Spring 会很聪明地帮助我们摆平 Test 依赖的对象,然后再运行已经“合法”的 Test Case,只要你在用于测试的配置文件里面定义了完整的依赖关系,一如其他正常对象。

<bean id=”Helloword” class=”Helloworld” autowire=”byType”/>


这样,经过上面三点变化,例子代码变成了这样:


import static org.easymock.EasyMock.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
test-context.xml)
public void HelloworldTest {
    @Autowired
    
private Foo foo;
    
    @Autowired
    
private Bar bar;
    
    @Autowired
    
private Helloworld helloworld;
    
    @Before
    
public void before() {
        reset(foo, bar);
    }
    
    @After
    
public void after() {
        verify(foo, bar);
    }
    
    @Test
    
public void shouldSayHello() {
        
//set expectations about foo/bar
        replay(foo, bar);
        
        helloworld.sayHello();
        
//assert verification
    }
    
    
//
}

2. 现在看上去是不是好多了?嗯,对象间的依赖关系和mock对象的创建都由 Spring 来替我们维护,再也不用费心了。不过,reset/verify 是不是还是看上去那么舒服?我们观察一下,通常为了简化对 mock 对象的验证,我们对 Test 类中使用到的 mock 对象都是一起reset/replay /verify,要是能有resetAll()/replayAll()/verifyAll()方法就好了,也省得不同的 Test 类写一大串对不同的 Mock 对象验证的方法。OK,这时候我们就要借助 Reflection 来完成这项任务了:通过 Reflection 得到 Test 类中所有加上 @Autowired 声明的成员变量,验证它们是不是由代理或者字节码增强,从而得到该 Test 类的所有由 Spring 创建的 mock 对象,进行 reset/replay/verify。

根据这个思路,我们引入这样一个 mock 测试的Helper类:

import static org.easymock.EasyMock.*;

final class MockTestHelper {

    public static void resetAll(Object testObject) {
        reset(getDeclaredMockedFields(testObject));
    }

    public static void verifyAll(Object testObject) {
        verify(getDeclaredMockedFields(testObject));
    }

    public static void replayAll(Object testObject) {
        replay(getDeclaredMockedFields(testObject));
    }

    private static Object[] getDeclaredMockedFields(Object testObject) {
        Field[] declaredFields 
= testObject.getClass().getDeclaredFields();
        List declaredMockedFields 
= new ArrayList();
        
for (Field field : declaredFields) {
            
if (field.isAnnotationPresent(Autowired.class)) {
                
boolean isAccessible = field.isAccessible();
                
try {
                    field.setAccessible(
true);
                    Object value 
= field.get(testObject);
                    
if (isClassProxy(value.getClass())) {
                        declaredMockedFields.add(value);
                    }
                } 
catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                
finally {
                    field.setAccessible(isAccessible);
                }
            }
        }
        
return declaredMockedFields.toArray();
    }

    private static boolean isClassProxy(Class clazz) {
        String className 
= clazz.getName();
        
return className.contains($Proxy|| className.contains($$EnhancerByCGLIB$$);
    }

}

好了,有了这么一个 Helper 类,写 mock 对象的Test 类就简单了许多。还是以上面的例子为例,经过这么一重构,变成如下:

这样看起来就好多了,以后不管在 Test 类里面添加多少个 Test 类需要的 mock 对象,我们都不需要再修改对 mock 对象的验证了,Helper类会自动帮我们完成所有的工作。
综上所述,使用Spring2.5里面引入的 Test Cntext 和 annotations 的确帮助我们减轻了大量的测试代码量,而且让我们的 Test 类更加POJO,更易于让人理解其职责,成为对 feature 的 specification。而 Reflection的小技巧,则能很方便的改进原来代码中不够动态的地方,进一步简化代码量和维护难度。当然我们可以看到,即使这样,代码里面还是有不少resetAll/replayAll/verifyAll的地方,作为 mock 框架带来的一些约束,我们没有办法来省略。这里推荐一种新的 mock 框架—— mockito,是有我的外国同事开发的,它不仅把mock、stub、spy等double的概念区分更清楚,而且让我们的 mock 测试更易写,更易读。

更多敬请期待本博的其他文章。

References:
Spring 2.5:Spring MVC中的新特性:http://www.infoq.com/cn/articles/spring-2.5-ii-spring-mvc
使用 Spring 2.5 TestContext 测试框架:https://www.ibm.com/developerworks/cn/java/j-lo-spring25-test/
mockito project homepage:http://code.google.com/p/mockito/