Category Archives: UnitTest

浅谈测试代码覆盖率

经常有人问这样的问题:“我们在做单元测试,那测试覆盖率要到多少才行?”。答案其实很简答,“作为指标的测试覆盖率都是没有用处的。”

Martin Fowler(重构那本书的作者)曾经写过一篇博客来讨论这个问题,他指出:把测试覆盖作为质量目标没有任何意义,而我们应该把它作为一种发现未被测试覆盖的代码的手段。

代码覆盖率的意义

Alt text

  1. 分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
  2. 检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
  3. 代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。

代码覆盖率工具

目前Java常用覆盖率工具Jacoco、Emma和Cobertura

覆盖率工具工作流程

Alt text 1. 对Java字节码进行插桩,On-The-Fly和Offine两种方式。
2. 执行测试用例,收集程序执行轨迹信息,将其dump到内存。
3. 数据处理器结合程序执行轨迹信息和代码结构信息分析生成代码覆盖率报告。
4. 将代码覆盖率报告图形化展示出来,如html、xml等文件格式。

插桩原理

Alt text

主流代码覆盖率工具都采用字节码插桩模式,通过钩子的方式来记录代码执行轨迹信息。其中字节码插桩又分为两种模式On-The-Fly和Offine。 On-The-Fly模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。Offine模式优点在于系统启动不需要额外开启 代理,但是只能在系统停机的情况下才能获取代码覆盖率。 基于以上特性,同时由于公司使用JDK8,我们采用Jacoco来获取集成测试代码覆盖率,单元测试使用Cobertura。

On-The-Fly插桩 Java Agent

  • JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序
  • 代理程序在每装载一个class文件前判断是否已经转换修改了该文件,如果没有则需要将探针插入class文件中。
  • 代码覆盖率就可以在JVM执行代码的时候实时获取。
  • 典型代表:Jacoco

On-The-Fly插桩 Class Loader

  • 自定义classloader实现自己的类装载策略,在类加载之前将探针插入class文件中
  • 典型代表:Emma

Offine插桩

  • 在测试之前先对文件进行插桩,生成插过桩的class文件或者jar包,执行插过桩的class文件或者jar包之后,会生成覆盖率信息到文件,最后统一对覆盖率信息进行处理,并生成报告。
  • Offline插桩又分为两种:
    • Replace:修改字节码生成新的class文件
    • Inject:在原有字节码文件上进行修改
  • 典型代表:Cobertura

On-The-Fly和Offine比较

  • On-The-Fly模式更加方便的获取代码覆盖率,无需提前进行字节码插桩,可以实时获取代码覆盖率信息
  • Offline模式适用于以下场景:
    • 运行环境不支持java agent
    • 部署环境不允许设置JVM参数
    • 字节码需要被转换成其他虚拟机字节码,如Android Dalvik VM
    • 动态修改字节码过程中和其他agent冲突
    • 无法自定义用户加载类

实践应用

单元测试覆盖率

目前有赞开发人员会写单元测试用例,为了能够引入持续集成,我们选取了Sonar+Cobertura来获取单元测试覆盖率。 我们将代码覆盖率绑定到代码编译阶段,这样每次代码编译就能够执行单元测试同时获取代码单元测试覆盖率

<plugin>  
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>cobertura-maven-plugin</artifactId>
    <version>2.7</version>
    <configuration>
        <formats>
            <format>xml</format>
        </formats>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>cobertura</goal>
            </goals>
        </execution>
    </executions>
</plugin>  

生成代码覆盖率文件以后,通过Jenkins SonarQube Scanner或者执行mvn sonar:sonar将该文件上传至Sonar 服务器,就可以解析该文件,生成图形化的界面

集成测试覆盖率

测试人员执行集成测试测试用例时(包括手工执行和自动化执行),我们需要代码覆盖率来发现测试用例设计的遗漏,及时补充用例来覆盖未被覆盖到的代码。

被测系统,在服务启动时,都会通过javaagent的方式做On-The-Fly插桩

  • 被测服务器启动之后,测试人员手工执行测试用例,Jacoco Agent会实时将代码覆盖率信息传输给Jacoco Prase Server,该服务器保存了被测代码源文件以及编译后的目标文件,服务器会结合源文件、目标文件以及代码覆盖率信息生成图表化的覆盖率文件。
  • 自动化执行测试用例完成之后,获取代码覆盖率信息,通过Jenkins Jacoco插件解析,获取图表化的覆盖率文件。 获取代码覆盖率报告之后,结合git获取的本次代码变动信息,得到测试用例覆盖的变动文件的测试覆盖率统计信息。来分析是否有由于测试用例设计遗漏导致的 代码没有覆盖或者是开发的无效代码导致该代码无法被覆盖,如果测试用例设计有所遗漏,可以对照的增加相应的用例;如果是无效代码可以删除。

自动化集成流程

Alt text 1. 业务开发完成之后,开发人员做单元测试,单元测试完成之后,保证单元测试全部通过同时单元测试代码覆盖率达到一定程度(这个需要开发和测试约定,理论上越高越好),开发提测。
2. 测试人员根据测试用例进行测试(包括手工测试和自动化测试),结合git获取本次变动代码的覆盖率信息。行覆盖率需达到100%,分支达到50%以上,这个需要具体场景具体分析。
3. 测试通过之后,代码合并至主干,进行自动化回归。
4. 回归测试通过之后,代码可以上线。

基于这套流程,我们可以将单元测试代码覆盖率和集成测试代码覆盖率整合到持续集成流程中,如果代码覆盖率达不到我们设置的某个值时,可以终止流程继续下去获取需要人工确认之后,继续流程。

总结

本文主要介绍了Java代码覆盖率统计原理以及结合有赞测试的工程实践介绍了代码覆盖率该如何应用的实际测试中。不管是白盒测试还是黑盒测试,代码 覆盖率统计都是必不可少的一环,它可以直接反映本次测试的遗漏点(不是100%反映)。结合到自动发布场景也是一个较好地衡量指标。

最后再重申一下本文开篇的观点:

  • 代码覆盖率统计是用来发现没有被测试覆盖的代码
  • 代码覆盖率统计不能完全用来衡量代码质量

from:http://tech.youzan.com/code-coverage/

JaCoCo:分析单元测试覆盖率的利器

前言

随着敏捷开发的流行,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者片面追求单元测试的数量,导致底下的开发人员投机取巧,编写出大量的重复测试,数量上去了,质量却依然原地踏步。相比单纯追求单元测试的数量,分析单元测试的代码覆盖率是一种更为可行的方式。JaCoCo(Java Code Coverage)就是一种分析单元测试覆盖率的工具,使用它运行单元测试后,可以给出代码中哪些部分被单元测试测到,哪些部分没有没测到,并且给出整个项目的单元测试覆盖情况百分比,看上去一目了然。EclEmma 是基于 JaCoCo 的一个 Eclipse 插件,开发人员可以方便的和其交互。因此,本文先从 EclEmma 入手,给读者一个直观的体验。


使用 EclEmma 在 Eclipse 中查看单元测试覆盖率

EclEmma 是基于 JaCoCo 的 Eclipse 插件,使用它,开发人员可以直观地看到单元测试的覆盖情况。

安装 EclEmma

打开 Eclipse 的软件市场,在其中搜索 EclEmma,找到后完成安装,如下图所示:

图 1. 安装 EclEmma

图 1. 安装 EclEmma

安装完成后,Eclipse 的工具条里会多出下面这样一个图标:

图 2. Coverage 图标

图 2. Coverage 图标

分析单元测试覆盖率

成功安装 EclEmma 后,就可以试着用它来分析项目的单元测试覆盖率了。为了方便演示,我们使用 Eclipse 创建了一个标准 Java 工程。其中包含一个数学工具类,用来计算三个数中的最大值,代码如下:

清单 1. 数学工具类
package com.dw.math;
public class MathUtil {
   public static int max(int a, int b, int c){
       if(a > b){
           if(a > c){
               return a;
           }else{
               return c;
           }
        }else{
           if(b > c){
               return b;
           }else{
               return c;
           }
        }
    }
}

可以看到,这里的算法稍微有点复杂,使用到了多个条件判断分支,因此,特别适合为其编写单元测试。第一个版本的单元测试如下:

清单 2. 第一个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
  public class MathUtilTest {
    @Test
    public void test_max_1_2_3() {
          assertEquals(3, MathUtil.max(1, 2, 3));
    }
}

试着运行一下单元测试覆盖率分析工具:40.0%!似乎不太理想。展开分析报告,双击后在编辑器里可以看到覆盖情况被不同的颜色标识出来,其中绿颜色表示代码被单元测试覆盖到,黄色表示部分覆盖,红色则表示完全没有覆盖到,如下图所示:

图 3. 单元测试覆盖率报告

图 3. 单元测试覆盖率报告

让我们尝试多加一些单元测试,来改善这种情况,请看下面第二个版本的单元测试:

清单 3. 第二个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
  public class MathUtilTest {
  @Test
  public void test_max_1_2_3() {
      assertEquals(3, MathUtil.max(1, 2, 3));
  }
  @Test
  public void test_max_10_20_30() {
      assertEquals(30, MathUtil.max(10, 20, 30));
  }
  @Test
  public void test_max_100_200_300() {
      assertEquals(300, MathUtil.max(100, 200, 300));
  }
}

测试覆盖率还是 40.0%!虽然我们额外再加了两个测试,但覆盖率没有半点提升,这些单元测试其实是重复的,它们在重复测试同一段代码。如果单纯追求单元测试的数量,那么这无疑会给管理者造成错觉,他们觉得单元测试的数量增加了,软件的质量更有保证了;而对于那些喜欢偷懒的程序员,也蒙混过关,但却给软件质量埋下了隐患。让我们删掉这些重复的单元测试,重新思考一下怎么测试这个方法。

首先我们要测试正常情况,这其中又包含 3 种情况:第一个参数最大,第二个参数最大,以及最后一个参数最大。然后我们还需测试几种特殊情况,比如三个参数相同,三个参数中,其中两个相同。让我们照此思路重新编写单元测试:

清单 4. 第三个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
public class MathUtilTest {
  @Test
  public void test_max_1_2_3() {
      assertEquals(3, MathUtil.max(1, 2, 3));
  }
  @Test
  public void test_max_1_3_2() {
      assertEquals(3, MathUtil.max(1, 3, 2));
  }
  @Test
  public void test_max_3_2_1() {
      assertEquals(3, MathUtil.max(3, 2, 1));
  }
  @Test
  public void test_max_0_0_0(){
      assertEquals(0, MathUtil.max(0, 0, 0));
  }
  @Test
  public void test_max_0_1_0(){
      assertEquals(1, MathUtil.max(0, 1, 0));
  }
}

再次运行单元测试分析工具:75.0%!这次比以前有了很大提升,但是结果还不能令人满意,打开分析报告可以看到,有一个分支还是没有覆盖到,如图所示:

图 4. 单元测试覆盖率报告

图 4. 单元测试覆盖率报告

阅读代码可以看出,这种情况是指第一个参数大于第二个参数,却小于第三个参数,因此我们再增加一个单元测试:

清单 5. 再增加一个单元测试
@Test
public void test_max_2_1_3() {
    assertEquals(3, MathUtil.max(2, 1, 3));
}

再运行一遍单元测试分析工具:100.0%!终于我们的单元测试达到了全覆盖,这样我们对自己开发的代码更有信心了。当然,我们在这里并不是为了单纯的追求这个数字,在增加单元测试覆盖率的诱导下,我们重新理清了测试的步骤,写出了更有意义、更全面的单元测试。而且根据单元测试分析工具给的反馈,我们还发现了先前没有想到的情形。因此,单元测试的覆盖率并不只是一个为了取悦管理者的数据,它实实在在地帮助我们改善了代码的质量,增加了我们对所编写代码的信心。


给管理者的单元测试覆盖率报告

管理者天生喜欢阅读报告。他们不会屈尊坐在你的办公桌前,让你向他展示 Eclipse 中这一片花花绿绿的东西。而且这份报告对他们至关重要,他们需要用它向上级汇报;年底回顾时,他们也可以兴奋地宣称产品的单元测试覆盖率增加了多少。作为一名开发人员,我们很大一部分工作量就在于满足管理者的这种需求。因此,本节我们讨论如何将 JaCoCo 集成到 Ant 脚本中,生成漂亮的单元测试覆盖率报告。

准备工作

在集成 JaCoCo 前,必须先确保你的 Java 工程有一个可执行的 Ant 构建脚本。一个简单的 Ant 构建脚本一般会执行如下任务:编译(包括编译工程代码和测试代码)、打包和执行单元测试。下面是本文示例 Java 项目所用的 Ant 构建脚本,读者可结合自己的项目及文件路径,在此基础之上进行修改。

清单 6. build.xml
<project name="math" basedir="." default="junit">
 <!--预定义的属性和 classpath -->
 <property name="src.dir" value="src" />
 <property name="test.dir" value="test" />
 <property name="build.dir" value="build" />
 <property name="classes.dir" value="${build.dir}/classes" />
 <property name="tests.dir" value="${build.dir}/tests" />
 <property name="jar.dir" value="${build.dir}/jar" />
 <property name="lib.dir" value="lib" />
 
 <path id="classpath">
     <fileset dir="${lib.dir}" includes="**/*.jar" />
 </path>
 
  <!--清除上次构建 -->
  <target name="clean">
      <delete dir="${build.dir}" />
  </target>
  
  <!--编译代码,包括单元测试 -->
  <target name="compile" depends="clean">
     <mkdir dir="${classes.dir}" />
     <mkdir dir="${tests.dir}" />
     <javac srcdir="${src.dir}" destdir="${classes.dir}" />
     <javac srcdir="${test.dir}" destdir="${tests.dir}">
     <classpath>
     <path refid="classpath" />
     <path location="${classes.dir}" />
     </classpath>
     </javac>
  </target>
  
  <!--打包 -->
  <target name="jar" depends="compile">
     <mkdir dir="${jar.dir}" />
     <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
     </jar>
  </target>
  
  <!--运行单元测试 -->
  <target name="junit" depends="jar">
     <junit printsummary="yes">
       <classpath>
       <path refid="classpath"/>
       <path location="${classes.dir}" />
       <path location="${tests.dir}" />
       </classpath>
       <batchtest fork="yes">
       <fileset dir="${test.dir}" includes="**/*Test.java"/>
       </batchtest>
     </junit>
  </target>
</project>

集成 JaCoCo

首先需要从 然后就是使用 JaCoCo 官网下载 需要的版本,然后将下载得到的压缩文件解压,将其中的 jacocoant.jar 拷贝至 Java 工程下存放第三方 jar 包的目录,在示例工程里,我有一个和 src 平级的 lib 目录,jacocoant.jar 就放到了这个目录底下,读者可根据自己的项目组织结构做相应调整。然后我们需要在 Ant 构建脚本中定义新的任务:

清单 7. 定义新的构建任务
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath refid="classpath" />
</taskdef>

现在就可以在 Ant 构建脚本中使用 JaCoCo 了。需要注意的是,为了避免命名冲突,需要给 Ant 构建脚本加入新的 XML 命名空间:

清单 8. 加入新的 JaCoCo 命名空间
<project name="math" basedir="." xmlns:jacoco="antlib:org.jacoco.ant" default="junit">

我们主要使用 JaCoCo 的两个任务:首先是jacoco:coverage,用来生成单元测试覆盖率数据,这是一个二进制文件,为了生成从该文件生成报表,我们还要调用另外一个任务jacoco:report,它的输入为jacoco:coverage生成的二进制文件,输出报表。报表有多种格式可选,可以是 HTML、XML、CSV 等。具体的脚本如下:

清单 9. 使用 JaCoCo 生成测试覆盖率和报表
<jacoco:coverage destfile="${build.dir}/jacoco.exec">
  <junit fork="true" forkmode="once" printsummary="yes">
     <classpath>
         <path refid="classpath" />
         <path location="${classes.dir}" />
         <path location="${tests.dir}" />
     </classpath>
     <batchtest fork="yes">
          <fileset dir="${test.dir}" includes="**/*Test.java"/>
     </batchtest>
  </junit>
 </jacoco:coverage>
 
 <jacoco:report>
   <executiondata>
        <file file="${build.dir}/jacoco.exec"/>
   </executiondata>
   <structure name="dw demo">
      <classfiles>
           <fileset dir="${classes.dir}"/>
      </classfiles>
      <sourcefiles encoding="UTF-8">
           <fileset dir="${src.dir}"/>
      </sourcefiles>
   </structure>
  <html destdir="${build.dir}"/>
</jacoco:report>

JaCoCo 的任务定义非常清晰,在这里略作说明。首先需要将原来的junit任务嵌入jacoco:coverage,而且需要指定fork="true",代表单元测试需要另起一个 JVM 执行,否则 JaCoCo 就会执行失败。destfile="${build.dir}/jacoco.exec"指定生成的测试覆盖率文件输出到什么地方,后面生成报告的时候需要输入该文件的地址。然后就是使用 jacoco:report 生成报告,指定前面任务生成的单元测试覆盖率文件、编译好的类文件以及源代码,最后选择一种格式,这里使用 html,生成报告。打开报告的存放路径,就可以看到如下所示的单元测试覆盖率报告:

图 5. HTML 版的单元测试覆盖率报告

图 5. HTML 版的单元测试覆盖率报告


和同类产品比较

市面上流行的单元测试覆盖率工具还有 CloverCobertura。和它们相比,JaCoCo 有如下优势:

  1. JaCoCo 拥有友好的授权形式。JaCoCo 使用了 Eclipse Public License,方便个人用户和商业用户使用。而 Clover 对于商业用户是收费的。
  2. JaCoCo 被良好地集成进各种工具中。在 Java 社区里,很多流行的工具都可以集成 JaCoCo,比如 SonarQube、Jenkins、Netbeans、Eclipse、IntelliJ IDEA、Gradle 等。
  3. JaCoCo 社区非常活跃,它是目前唯一支持 Java 8 的单元测试覆盖率工具。而且关于 JaCoCo 的文档相对较多,降低了学习门槛。

结束语

本文为大家介绍了如何使用 JaCoCo 分析项目的单元测试覆盖率,文章先从 JaCoCo 的 Eclipse 插件 EclEmma 开始,直观地介绍了如何一步步提高单元测试质量,最终达到对代码的全覆盖;然后为大家介绍了如何将 JaCoCo 集成到 Ant 构建脚本中,生成漂亮的单元测试覆盖率报告。但是使用 JaCoCo 只是第一步,重要的是开发人员能够根据工具所给的反馈,不断改进自己的单元测试,写出高质量的代码。

参考资料

学习

from:http://www.ibm.com/developerworks/cn/java/j-lo-jacoco/index.html

 

使用 Spring 进行单元测试

概述

单元测试和集成测试在我们的软件开发整个流程中占有举足轻重的地位,一方面,程序员通过编写单元测试来验证自己程序的有效性,另外一方面,管理者通过持续自动的执行单元测试和分析单元测试的覆盖率等来确保软件本身的质量。这里,我们先不谈单元测试本身的重要性,对于目前大多数的基于 Java 的企业应用软件来说,Spring 已经成为了标准配置,一方面它实现了程序之间的低耦合度,另外也通过一些配置减少了企业软件集成的工作量,例如和 Hibernate、Struts 等的集成。那么,有个问题,在普遍使用 Spring 的应用程序中,我们如何去做单元测试?或者说,我们怎么样能高效的在 Spring 生态系统中实现各种单元测试手段?这就是本文章要告诉大家的事情。

单元测试目前主要的框架包括 Junit、TestNG,还有些 MOCK 框架,例如 Jmock、Easymock、PowerMock 等,这些都是单元测试的利器,但是当把他们用在 Spring 的开发环境中,还是那么高效么?还好,Spring 提供了单元测试的强大支持,主要特性包括:

  • 支持主流的测试框架 Junit 和 TestNG
  • 支持在测试类中使用依赖注入 Denpendency Injection
  • 支持测试类的自动化事务管理
  • 支持使用各种注释标签,提高开发效率和代码简洁性
  • Spring 3.1 更是支持在测试类中使用非 XML 配置方法和基于 Profile 的 bean 配置模式

通过阅读本文,您能够快速的掌握基于 Spring TestContext 框架的测试方法,并了解基本的实现原理。本文将提供大量测试标签的使用方法,通过这些标签,开发人员能够极大的减少编码工作量。OK,现在让我们开始 Spring 的测试之旅吧!


原来我们是怎么做的

这里先展示一个基于 Junit 的单元测试,这个单元测试运行在基于 Spring 的应用程序中,需要使用 Spring 的相关配置文件来进行测试。相关类图如下:

数据库表

假设有一个员工账号表,保存了员工的基本账号信息,表结构如下:

  • ID:整数类型,唯一标识
  • NAME:字符串,登录账号
  • SEX:字符串,性别
  • AGE:字符串,年龄

假设表已经建好,且内容为空。

测试工程目录结构和依赖 jar 包

在 Eclipse 中,我们可以展开工程目录结构,看到如下图所示的工程目录结构和依赖的 jar 包列表:

您需要引入的 jar 包括:

  • cglib-nodep-2.2.3.jar
  • commons-logging.jar
  • hsqldb.jar
  • Junit-4.5.jar
  • log4j-1.2.14.jar
  • Spring-asm-3.2.0.M1.jar
  • Spring-beans-3.2.0.M1.jar
  • Spring-context-3.2.0.M1.jar
  • Spring-core-3.2.0.M1.jar
  • Spring-expression-3.2.0.M1.jar
  • Spring-jdbc-3.2.0.M1.jar
  • Spring-test-3.2.0.M1.jar
  • Spring-tx-3.2.0.M1.jar
  • testng-6.8.jar

其中的 hsqldb 是我们测试用数据库。

图 1. 工程目录结构

图 1. 工程目录结构

类总体介绍

假设我们现在有一个基于 Spring 的应用程序,除了 MVC 层,还包括业务层和数据访问层,业务层有一个类 AccountService,负责处理账号类的业务,其依赖于数据访问层 AccountDao 类,此类提供了基于 Spring Jdbc Template 实现的数据库访问方法,AccountService 和 AccountDao 以及他们之间的依赖关系都是通过 Spring 配置文件进行管理的。

现在我们要对 AccountService 类进行测试,在不使用 Spring 测试方法之前,我们需要这样做:

此类代表账号的基本信息,提供 getter 和 setter 方法。

清单 1. Account.Java
 package domain; 

 public class Account { 
	 public static final String SEX_MALE = "male"; 
	 public static final String SEX_FEMALE = "female"; 
	
	 private int id; 
	 private String name; 
	 private int age; 
	 private String sex; 
     public String toString() { 
	    return String.format("Account[id=%d,name=%s,age:%d,sex:%s]",id,name,age,sex); 
	 } 
	 public int getId() { 
		 return id; 
	 } 
	 public void setId(int id) { 
		 this.id = id; 
	 } 
	 public String getName() { 
		 return name; 
	 } 
	 public void setName(String name) { 
		 this.name = name; 
	 } 
	 public int getAge() { 
		 return age; 
	 } 
	 public void setAge(int age) { 
		 this.age = age; 
	 } 
	 public String getSex() { 
		 return sex; 
	 } 
	 public void setSex(String sex) { 
		 this.sex = sex; 
	 } 
	
     public static Account getAccount(int id,String name,int age,String sex) { 
		 Account acct = new Account(); 
		 acct.setId(id); 
		 acct.setName(name); 
		 acct.setAge(age); 
		 acct.setSex(sex); 
		 return acct; 
	 } 
 }

注意上面的 Account 类有一个 toString() 方法和一个静态的 getAccount 方法,getAccount 方法用于快速获取 Account 测试对象。

这个 DAO 我们这里为了简单起见,采用 Spring Jdbc Template 来实现。

清单 2. AccountDao.Java
 package DAO; 

 import Java.sql.ResultSet; 
 import Java.sql.SQLException; 
 import Java.util.HashMap; 
 import Java.util.List; 
 import Java.util.Map; 

 import org.Springframework.context.ApplicationContext; 
 import org.Springframework.context.support.ClassPathXmlApplicationContext; 
 import org.Springframework.jdbc.core.RowMapper; 
 import org.Springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; 
 import org.Springframework.jdbc.core.simple.ParameterizedRowMapper; 

 import domain.Account; 

 public class AccountDao extends NamedParameterJdbcDaoSupport { 
	 public void saveAccount(Account account) { 
		 String sql = "insert into tbl_account(id,name,age,sex) " + 
				"values(:id,:name,:age,:sex)"; 
		 Map paramMap = new HashMap(); 
		 paramMap.put("id", account.getId()); 
		 paramMap.put("name", account.getName()); 
		 paramMap.put("age", account.getAge()); 
		 paramMap.put("sex",account.getSex()); 
		 getNamedParameterJdbcTemplate().update(sql, paramMap); 
	 } 
	
	 public Account getAccountById(int id) { 
		 String sql = "select id,name,age,sex from tbl_account where id=:id"; 
		 Map paramMap = new HashMap(); 
		 paramMap.put("id", id); 
		 List<Account> matches = getNamedParameterJdbcTemplate().query(sql, 
		 paramMap,new ParameterizedRowMapper<Account>() { 
					 @Override 
					 public Account mapRow(ResultSet rs, int rowNum) 
							 throws SQLException { 
						 Account a = new Account(); 
						 a.setId(rs.getInt(1)); 
						 a.setName(rs.getString(2)); 
						 a.setAge(rs.getInt(3)); 
						 a.setSex(rs.getString(4)); 
						 return a; 
					 } 
			
		 }); 
		 return matches.size()>0?matches.get(0):null; 
	 } 
	
 }

AccountDao 定义了几个账号对象的数据库访问方法:

  • saveAccount:负责把传入的账号对象入库
  • getAccountById:负责根据 Id 查询账号
清单 3. AccountService.Java
 package service; 

 import org.apache.commons.logging.Log; 
 import org.apache.commons.logging.LogFactory; 
 import org.Springframework.beans.factory.annotation.Autowired; 

 import DAO.AccountDao; 
 import domain.Account; 

 public class AccountService { 
	 private static final Log log = LogFactory.getLog(AccountService.class); 
	
	 @Autowired 
	 private AccountDao accountDao; 
	
	 public Account getAccountById(int id) { 
		 return accountDao.getAccountById(id); 
	 } 
	
	 public void insertIfNotExist(Account account) { 
		 Account acct = accountDao.getAccountById(account.getId()); 
		 if(acct==null) { 
			 log.debug("No "+account+" found,would insert it."); 
             accountDao.saveAccount(account); 
		 } 
		 acct = null; 
	 } 
		
 }

AccountService 包括下列方法:

  • getAccountById:根据 Id 查询账号信息
  • insertIfNotExist:根据传入的对象插入数据库

其依赖的 DAO 对象 accountDao 是通过 Spring 注释标签 @Autowired 自动注入的。

上述几个类的依赖关系是通过 Spring 进行管理的,配置文件如下:

清单 4. Spring 配置文件
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.Springframework.org/schema/context"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd 
 http://www.Springframework.org/schema/context 
 http://www.Springframework.org/schema/context/Spring-context-3.0.xsd "> 
	
 <context:annotation-config/> 
 <bean id="datasource" class="
 org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa" /> 
		 <property name="password" value="" /> 
	 </bean> 
	 <bean id="initer" init-method="init" class="service.Initializer"> 
	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
		 <property name="dataSource" ref="datasource" /> 
	 </bean> 
 <bean id="accountService" class="service.AccountService"> 
	 </bean> 
 </beans>

注意其中的“<context:annotation-config/>”的作用,这个配置启用了 Spring 对 Annotation 的支持,这样在我们的测试类中 @Autowired 注释才会起作用(如果用了 Spring 测试框架,则不需要这样的配置项,稍后会演示)。另外还有一个 accountDao 依赖的 initer bean, 这个 bean 的作用是加载 log4j 日志环境,不是必须的。

另外还有一个要注意的地方,就是 datasource 的定义,由于我们使用的是 Spring Jdbc Template,所以只要定义一个 org.Springframework.jdbc.datasource.DriverManagerDataSource 类型的 datasource 即可。这里我们使用了简单的数据库 HSQL、Single Server 运行模式,通过 JDBC 进行访问。实际测试中,大家可以选择 Oracle 或者 DB2、Mysql 等。

好,万事具备,下面我们来用 Junit4 框架测试 accountService 类。代码如下:

清单 5. AccountServiceOldTest.Java
 package service; 

 import static org.Junit.Assert.assertEquals; 

 import org.Junit.BeforeClass; 
 import org.Junit.Test; 
 import org.Springframework.context.ApplicationContext; 
 import org.Springframework.context.support.ClassPathXmlApplicationContext; 

 import domain.Account; 


 public class AccountServiceOldTest { 
	 private static AccountService service; 
	
	 @BeforeClass 
	 public static void init() { 
		 ApplicationContext 
 context = new ClassPathXmlApplicationContext("config/Spring-db-old.xml"); 
		 service = (AccountService)context.getBean("accountService"); 
	 } 	
	
	 @Test 
	 public void testGetAcccountById() { 
 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 Account acct2 = null; 
		 try { 
 service.insertIfNotExist(acct); 
			 acct2 = service.getAccountById(1); 
			 assertEquals(acct, acct2); 
		 } catch (Exception ex) { 
			 fail(ex.getMessage()); 
		 } finally { 
			 service.removeAccount(acct); 
		 } 
 } 
 }

注意上面的 Junit4 注释标签,第一个注释标签 @BeforeClass,用来执行整个测试类需要一次性初始化的环境,这里我们用 Spring 的 ClassPathXmlApplicationContext 从 XML 文件中加载了上面定义的 Spring 配置文件,并从中获得了 accountService 的实例。第二个注释标签 @Test 用来进行实际的测试。

测试过程:我们先获取一个 Account 实例对象,然后通过 service bean 插入数据库中,然后通过 getAccountById 方法从数据库再查询这个记录,如果能获取,则判断两者的相等性;如果相同,则表示测试成功。成功后,我们尝试删除这个记录,以利于下一个测试的进行,这里我们用了 try-catch-finally 来保证账号信息会被清除。

执行测试:(在 Eclipse 中,右键选择 AccountServiceOldTest 类,点击 Run as Junit test 选项),得到的结果如下:

执行测试的结果

在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

图 2. 测试的结果

图 2. 测试的结果

对于这种不使用 Spring test 框架进行的单元测试,我们注意到,需要做这些工作:

  • 在测试开始之前,需要手工加载 Spring 的配置文件,并获取需要的 bean 实例
  • 在测试结束的时候,需要手工清空搭建的数据库环境,比如清除您插入或者更新的数据,以保证对下一个测试没有影响

另外,在这个测试类中,我们还不能使用 Spring 的依赖注入特性。一切都靠手工编码实现。好,那么我们看看 Spring test 框架能做到什么。

首先我们修改一下 Spring 的 XML 配置文件,删除 <context:annotation-config/> 行,其他不变。

清单 6. Spring-db1.xml
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.2.xsd"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa"/> 
		 <property name="password" value=""/> 
	 </bean> 
 <bean id="transactionManager" 
 class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
 		 <property name="dataSource" ref="datasource"></property> 
 	 </bean> 
 	 <bean id="initer" init-method="init" class="service.Initializer"> 
 	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
 		 <property name="dataSource" ref="datasource"/> 
 	 </bean> 
 	 <bean id="accountService" class="service.AccountService"> 
 	 </bean> 
 </beans>

其中的 transactionManager 是 Spring test 框架用来做事务管理的管理器。

清单 7. AccountServiceTest1.Java
 package service; 
 import static org.Junit.Assert.assertEquals; 

 import org.Junit.Test; 
 import org.Junit.runner.RunWith; 
 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.test.context.ContextConfiguration; 
 import org.Springframework.test.context.Junit4.SpringJUnit4ClassRunner; 
 import org.Springframework.transaction.annotation.Transactional; 

 import domain.Account; 

 @RunWith(SpringJUnit4ClassRunner.class) 
 @ContextConfiguration("/config/Spring-db1.xml") 
 @Transactional 
 public class AccountServiceTest1 { 
	 @Autowired 
	 private AccountService service; 
	
	 @Test 
	 public void testGetAcccountById() { 
 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 service.insertIfNotExist(acct); 
		 Account acct2 = service.getAccountById(1); 
		 assertEquals(acct,acct2); 
	 } 
 }

对这个类解释一下:

  • @RunWith 注释标签是 Junit 提供的,用来说明此测试类的运行者,这里用了 SpringJUnit4ClassRunner,这个类是一个针对 Junit 运行环境的自定义扩展,用来标准化在 Spring 环境中 Junit4.5 的测试用例,例如支持的注释标签的标准化
  • @ContextConfiguration 注释标签是 Spring test context 提供的,用来指定 Spring 配置信息的来源,支持指定 XML 文件位置或者 Spring 配置类名,这里我们指定 classpath 下的 /config/Spring-db1.xml 为配置文件的位置
  • @Transactional 注释标签是表明此测试类的事务启用,这样所有的测试方案都会自动的 rollback,即您不用自己清除自己所做的任何对数据库的变更了
  • @Autowired 体现了我们的测试类也是在 Spring 的容器中管理的,他可以获取容器的 bean 的注入,您不用自己手工获取要测试的 bean 实例了
  • testGetAccountById 是我们的测试用例:注意和上面的 AccountServiceOldTest 中相同的测试方法的对比,这里我们不用再 try-catch-finally 了,事务管理自动运行,当我们执行完成后,所有相关变更会被自动清除

执行结果

在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

图 3. 执行结果

图 3. 执行结果

小结

如果您希望在 Spring 环境中进行单元测试,那么可以做如下配置:

  • 继续使用 Junit4 测试框架,包括其 @Test 注释标签和相关的类和方法的定义,这些都不用变
  • 您需要通过 @RunWith(SpringJUnit4ClassRunner.class) 来启动 Spring 对测试类的支持
  • 您需要通过 @ContextConfiguration 注释标签来指定 Spring 配置文件或者配置类的位置
  • 您需要通过 @Transactional 来启用自动的事务管理
  • 您可以使用 @Autowired 自动织入 Spring 的 bean 用来测试

另外您不再需要:

  • 手工加载 Spring 的配置文件
  • 手工清理数据库的每次变更
  • 手工获取 application context 然后获取 bean 实例

Spring 测试注释标签

我们已经看到利用 Spring test framework 来进行基于 Junit4 的单元测试是多么的简单,下面我们来看一下前面遇到的各种注释标签的一些可选用法。

@ContextConfiguration 和 @Configuration 的使用

刚才已经介绍过,可以输入 Spring xml 文件的位置,Spring test framework 会自动加载 XML 文件,得到 application context,当然也可以使用 Spring 3.0 新提供的特性 @Configuration,这个注释标签允许您用 Java 语言来定义 bean 实例,举个例子:

现在我们将前面定义的 Spring-db1.xml 进行修改,我们希望其中的三个 bean:initer、accountDao、accountService 通过配置类来定义,而不是 XML,则我们需要定义如下配置类:

注意:如果您想使用 @Configuration,请在 classpath 中加入 cglib 的 jar 包(cglib-nodep-2.2.3.jar),否则会报错。

清单 8. SpringDb2Config.Java
 package config; 

 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.context.annotation.Bean; 
 import org.Springframework.context.annotation.Configuration; 
 import org.Springframework.jdbc.datasource.DriverManagerDataSource; 

 import service.AccountService; 
 import service.Initializer; 
 import DAO.AccountDao; 

 @Configuration 
 public class SpringDb2Config { 
	 private @Autowired DriverManagerDataSource datasource; 
	 @Bean 
	 public Initializer initer() { 
		 return new Initializer(); 
	 } 
	
	 @Bean 
	 public AccountDao accountDao() { 
 AccountDao DAO = new AccountDao(); 
 DAO.setDataSource(datasource); 
 return DAO; 
	 } 
	
	 @Bean 
	 public AccountService accountService() { 
 return new AccountService(); 
	 } 
 }

注意上面的注释标签:

  • @Configuration:表明这个类是一个 Spring 配置类,提供 Spring 的 bean 定义,实际效果等同于 XML 配置方法
  • @Bean:表明这个方法是一个 bean 的定义,缺省情况下,方法名称就是 bean 的 Id
  • @Autowired:这个 datasource 采用自动注入的方式获取

注意,我们采用的是 XML+config bean 的方式进行配置,这种方式比较符合实际项目的情况。相关的 Spring 配置文件也要做变化,如下清单所示:

清单 9. Spring-db2.xml
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.Springframework.org/schema/context"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd 
 http://www.Springframework.org/schema/context 
 http://www.Springframework.org/schema/context/Spring-context-3.0.xsd"> 
 <context:annotation-config/> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa"/> 
		 <property name="password" value=""/> 
	 </bean> 
 <bean id="transactionManager" 
          class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
 		 <property name="dataSource" ref="datasource"></property> 
 	 </bean> 
 	
 	 <bean class="config.SpringDb2Config"/> 
 </beans>

注意里面的 context 命名空间的定义,如代码中黑体字所示。另外还必须有 <context:annotaiton-config/> 的定义,这个定义允许采用注释标签的方式来控制 Spring 的容器,最后我们看到 beans 已经没有 initer、accountDao 和 accountService 这些 bean 的定义,取而代之的是一个 SpringDb2Config bean 的定义,注意这个 bean 没有名称,因为不需要被引用。

现在有了这些配置,我们的测试类只要稍稍修改一下,即可实现加载配置类的效果,如下:

 @ContextConfiguration("/config/Spring-db2.xml")

通过上面的配置,测试用例就可以实现加载 Spring 配置类,运行结果也是成功的 green bar。

@DirtiesContext

缺省情况下,Spring 测试框架一旦加载 applicationContext 后,将一直缓存,不会改变,但是,

由于 Spring 允许在运行期修改 applicationContext 的定义,例如在运行期获取 applicationContext,然后调用 registerSingleton 方法来动态的注册新的 bean,这样的情况下,如果我们还使用 Spring 测试框架的被修改过 applicationContext,则会带来测试问题,我们必须能够在运行期重新加载 applicationContext,这个时候,我们可以在测试类或者方法上注释:@DirtiesContext,作用如下:

  • 如果定义在类上(缺省),则在此测试类运行完成后,重新加载 applicationContext
  • 如果定义在方法上,即表示测试方法运行完成后,重新加载 applicationContext

@TransactionConfiguration 和 @Rollback

缺省情况下,Spring 测试框架将事务管理委托到名为 transactionManager 的 bean 上,如果您的事务管理器不是这个名字,那需要指定 transactionManager 属性名称,还可以指定 defaultRollback 属性,缺省为 true,即所有的方法都 rollback,您可以指定为 false,这样,在一些需要 rollback 的方法,指定注释标签 @Rollback(true)即可。


对 Junit4 的注释标签支持

看了上面 Spring 测试框架的注释标签,我们来看看一些常见的基于 Junit4 的注释标签在 Spring 测试环境中的使用方法。

@Test(expected=…)

此注释标签的含义是,这是一个测试,期待一个异常的发生,期待的异常通过 xxx.class 标识。例如,我们修改 AccountService.Java 的 insertIfNotExist 方法,对于传入的参数如果为空,则抛出 IllegalArgumentException,如下:

 public void insertIfNotExist(Account account) { 
	 if(account==null) 
		 throw new IllegalArgumentException("account is null"); 
	 Account acct = accountDao.getAccountById(account.getId()); 
	 if(acct==null) { 
		 log.debug("No "+account+" found,would insert it."); 
		 accountDao.saveAccount(account); 
	 } 
	 acct = null; 
 }

然后,在测试类中增加一个测试异常的方法,如下:

 @Test(expected=IllegalArgumentException.class) 
 public void testInsertException() { 
	 service.insertIfNotExist(null); 
 }

运行结果是 green bar。

@Test(timeout=…)

可以给测试方法指定超时时间(毫秒级别),当测试方法的执行时间超过此值,则失败。

比如在 AccountService 中增加如下方法:

 public void doSomeHugeJob() { 
	 try { 
		 Thread.sleep(2*1000); 
	 } catch (InterruptedException e) { 
	 } 
 }

上述方法模拟任务执行时间 2 秒,则测试方法如下:

 @Test(timeout=3000) 
 public void testHugeJob() { 
	 service.doSomeHugeJob(); 
 }

上述测试方法期待 service.doSomeHugeJob 方法能在 3 秒内结束,执行测试结果是 green bar。

@Repeat

通过 @Repeat,您可以轻松的多次执行测试用例,而不用自己写 for 循环,使用方法:

 @Repeat(3) 
 @Test(expected=IllegalArgumentException.class) 
 public void testInsertException() { 
	 service.insertIfNotExist(null); 
 }

这样,testInsertException 就能被执行 3 次。


在测试类中基于 profile 加载测试 bean

从 Spring 3.2 以后,Spring 开始支持使用 @ActiveProfiles 来指定测试类加载的配置包,比如您的配置文件只有一个,但是需要兼容生产环境的配置和单元测试的配置,那么您可以使用 profile 的方式来定义 beans,如下:

清单 10. Spring-db.xml
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.2.xsd"> 
 	 <beans profile="test"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
	 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
	 <property name="username" value="sa"/> 
	 <property name="password" value=""/> 
	 </bean> 
</beans> 
	
<beans profile="production"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
	 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
	 <property name="url" value="jdbc:hsqldb:hsql://localhost/prod" /> 
	 <property name="username" value="sa"/> 
	 <property name="password" value=""/> 
	 </bean> 
 </beans> 
 <beans profile="test,production"> 
 <bean id="transactionManager" 
     class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
	 <property name="dataSource" ref="datasource"></property> 
	 </bean> 
	 <bean id="initer" init-method="init" class="service.Initializer"> 
	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
	 		 <property name="dataSource" ref="datasource"/> 
	 	 </bean> 
	 	
	 	 <bean id="accountService" class="service.AccountService"> 
	 	 </bean> 
	 	 <bean id="envSetter" class="EnvSetter"/> 
 	 </beans> 
 </beans>

上面的定义,我们看到:

  • 在 XML 头中我们引用了 Spring 3.2 的 beans 定义,因为只有 Spring 3.2+ 才支持基于 profile 的定义
  • 在 <beans> 根节点下可以嵌套 <beans> 定义,要指定 profile 属性,这个配置中,我们定义了两个 datasource,一个属于 test profile,一个输入 production profile,这样,我们就能在测试程序中加载 test profile,不影响 production 数据库了
  • 在下面定义了一些属于两个 profile 的 beans,即 <beans profile=”test,production”> 这样方便重用一些 bean 的定义,因为这些 bean 在两个 profile 中都是一样的
清单 11. AccountServiceTest.Java
 @RunWith(SpringJUnit4ClassRunner.class) 
 @ContextConfiguration("/config/Spring-db.xml") 
 @Transactional 
 @ActiveProfiles("test") 
 public class AccountServiceTest { 
 ... 
 }

注意上面的 @ActiveProfiles,可以指定一个或者多个 profile,这样我们的测试类就仅仅加载这些名字的 profile 中定义的 bean 实例。


对 TestNG 的支持

Spring 2.5 以后,就开始支持 TestNG 了,支持的方法包括:

  • 将您的 TestNG 测试类继承 Spring 的测试父类:AbstractTransactionalTestNGSpringContextTests 或者 AbstractTestNGSpringContextTests,这样您的 TestNG 测试类内部就可以访问 applicationContext 成员变量了
  • 不继承 Spring 父类,在测试类上使用 @TestExecutionListeners 注释标签,可以引入的监听器包括
    • DependencyInjectionTestExecutionListener:使得测试类拥有依赖注入特性
    • DirtiesContextTestExecutionListener:使得测试类拥有更新 applicationContext 能力
    • TransactionalTestExecutionListener:使得测试类拥有自动的事务管理能力

这里我们演示一下如何使用 Spring 提供的 TestNG 父类来进行测试。

清单 12. AccountServiceTestNGTest.Java
 package testng; 

 import static org.Junit.Assert.assertEquals; 

 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.test.context.ActiveProfiles; 
 import org.Springframework.test.context.ContextConfiguration; 
 import org.Springframework.test.context.testng. 
 AbstractTransactionalTestNGSpringContextTests; 
 import org.Springframework.transaction.annotation.Transactional; 

 import service.AccountService; 
 import domain.Account; 

 @ContextConfiguration("/config/Spring-db.xml") 
 @Transactional 
 @ActiveProfiles("test") 
 public class AccountServiceTestNGTest extends 
 AbstractTransactionalTestNGSpringContextTests { 
	 @Autowired 
	 private AccountService service; 
	
	 @org.testng.annotations.Test 
	 public void testGetAcccountById() { 
		 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 service.insertIfNotExist(acct); 
		 Account acct2 = service.getAccountById(1); 
		 assertEquals(acct,acct2); 
	 } 
 }

执行测试,我们将看到测试成功。

图 4. 测试成功

图 4. 测试成功

搜索数据库对应的表,我们看到里面没有数据,说明自动事务起作用了。


基本原理

Spring test framework 主要位于 org.Springframework.test.context 包中,主要包括下面几个类:

图 5. Spring 测试框架类图(查看大图

图 5. Spring 测试框架类图

  • TestContextManager:主要的入口类,提供 TestContext 实例的管理,负责根据各种事件来通知测试监听器
  • TestContext:实体类,提供访问 Spring applicatoin context 的能力,并负责缓存 applicationContext
  • TestExecutionListener:测试监听器,提供依赖注入、applicationContext 缓存和事务管理等能力
  • ContextLoader:负责根据配置加载 Spring 的 bean 定义,以构建 applicationContext 实例对象
  • SmartContextLoader:Spring 3.1 引入的新加载方法,支持按照 profile 加载

Spring 通过 AOP hook 了测试类的实例创建、beforeClass、before、after、afterClass 等事件入口,执行顺序主要如下:

图 6. Spring 测试框架执行序列图(查看大图

图 6. Spring 测试框架执行序列图

  • 测试执行者开始执行测试类,这个时候 Spring 获取消息,自动创建 TestContextManager 实例
  • TestContextManager 会创建 TestContext,以记录当前测试的上下文信息,TestContext 则通过 ContextLoader 来获取 Spring ApplicationContext 实例
  • 当测试执行者开始执行测试类的 BeforeClass、Before、After、AfterClass 的时候,TestContextManager 将截获事件,发通知给对应的 TestExecutionListener

总结

根据上面的例子和介绍,我们可以看到,Spring 测试框架的主要特点如下:

  • 完美的支持了 Junit4(提供特别的 SpringJunit4ClassRunner),比较好的支持了 TestNG
  • 在支持原有单元测试能力的基础上,通过各种监听器,支持了测试类的依赖注入、对 Spring applicationContext 的访问以及事务管理能力,为使用 Spring 架构的应用程序的测试带来了极大的便利性
  • Spring 3.1 引入的基于 profile 的加载能力使得测试环境和正式环境可以在一个 XML 定义中完美的结合

总之,如果您的程序中使用了 Spring,且对用 Junit 或者 testNG 来对他们进行单元测试感到力不从心,可以考虑使用 Spring test framework,它将使您的应用程序的质量上一个新的台阶。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区:查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

其他参考:
Using Spring MVC Test to unit test multipart POST request
Unit Testing of Spring MVC Controllers: REST API

from:http://www.ibm.com/developerworks/cn/java/j-lo-springunitest/index.html