这些文章摘自于我的 《Spring 3.x企业应用开发实战》的第16章,我将通过连载的方式,陆续在此发出。欢迎大家讨论。
一种商品只有通过严格检测才能投放市场,一架飞机只有经过严格测试才能上天,同样的,一款软件只有对其各项功能进行严格测试后才能交付使用。不管一个软件多么复杂,它都是由相互关联的方法和类组成的,每个方法和类都可能隐藏着Bug。只有防微杜渐,小步前进才可以保证软件大厦的稳固性,否则隐藏在类中的Bug随时都有可能像打开的潘多拉魔盒一样让程序陷于崩溃之中,难以驾驭。
按照软件工程思想,软件测试可以分为单元测试、集成测试、功能测试、系统测试等。功能测试和系统测试一般来说是测试人员的职责,但单元测试和集成测试则必须由开发人员保证。
为什么需要单元测试
软件开发的标准过程包括以下几个阶段:『需求分析阶段』→『设计阶段』→『实现阶段』→『测试阶段』→『发布』。其中测试阶段通过人工或者自动手段来运行或测试某个系统的过程,其目的在于检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别。测试过程按4个步骤进行,即单元测试、集成测试、系统测试及发版测试。其中功能测试主要检查已实现的软件是否满足了需求规格说明中确定了的各种需求,以及软件功能是否完全、正确。系统测试主要对已经过确认的软件纳入实际运行环境中,与其他系统成份组合在一起进行测试。单元测试、集成测试由开发人员进行,是我们关注的重点,下文对两者进行详细说明。
单元测试
单元测试是开发者编写的一小段代码,用于检验目标代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试用于判断某个特定条件或特定场景下某个特定函数的行为。例如,用户可能把一个很大的值放入一个有序List中,然后确认该值出现在List 的尾部。或者,用户可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
在一般情况下,一个功能模块往往会调用其他功能模块完成某项功能,如业务层的业务类可能会调用多个DAO完成某项业务。对某个功能模块进行单元测试时,我们希望屏蔽对外在功能模块的依赖,以便将焦点放在目标功能模块的测试上。这时模拟对象将是最有力的工具,它根据外在模块的接口模拟特定操作行为,这样单元测试就可以在假设关联模块正确工作的情况下验证本模块逻辑的正确性了。
集成测试
单元测试和开发工作是并驾齐驱的工作,甚至是前置性的工作。除了一些显而易见的功能外,大部分功能(类的方法)都必须进行单元测试,通过单元测试可以保障功能模块的正确性。而集成测试则是在功能模块开发完成后,为验证功能模块之间匹配调用的正确性而进行的测试。在单元测试时,往往需要通过模拟对象屏蔽外在模块的依赖,而集成测试恰恰是要验证模块之间集成后的正确性。
举个例子,当对UserService这个业务层的类进行单元测试时,可以通过创建UserDao、LoginLogDao模拟对象,在假设DAO类正确工作的情况下对UserService进行测试。而对UserService进行集成测试时,则应该注入真实的UserDao和LoginLogDao进行测试。
所以一般来讲,集成测试面向的层面要更高一些,一般对业务层和Web层进行集成测试,单元测试则面向一些功能单一的类(如字符串格式化工具类、数据计算类)。当然,我们可能对某一个类既进行单元测试又进行集成测试,如UserService在模块开发期间进行单元测试,而在关联的DAO类开发完成后,再进行集成测试。
测试好处
在编写代码的过程中,一定会反复调试保证它能够编译通过。但代码通过编译,只是说明了它的语法正确。无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。幸运的是,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信地交付自己的代码,减少后顾之忧。总之进行单元测试,会带来以下好处:
- 软件质量最简单、最有效的保证;
- 是目标代码最清晰、最有效的文档;
- 可以优化目标代码的设计;
- 是代码重构的保障;
- 是回归测试和持续集成的基石。
单元测试之误解
认为单元测试影响开发进度,一是借口,拒绝对单元测试相关知识进行学习(单元测试,代码重构,版本管理是开发人员的必备);二是单元测试是“先苦后甜”,刚开始搭建环境,引入额外工作,看似“影响进度”,但长远来看,由于程序质量提升、代码返工减少、后期维护工作量缩小、项目风险降低,从而在整体上赢了回来。
误解1:影响开发进度
一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到系统实际运行效果。这在外表上看来好像加快进度,而像单元测试这样的活动被看作是影响进度原因之一,推迟了对整个系统进行集成测试的时间。
在实践中,这种开发步骤常常会导致这样的结果:软件甚至无法运行。更进一步的结果是大量的时间将被花费在跟踪那些包含在独立单元里的简单Bug上面,在个别情况下,这些Bug也许是琐碎和微不足道的,但是总的来说,它们会导致推迟软件产品交付的时间,而且也无法确保它能够可靠运行。
在实际工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效利用。
误解2:增加开发成本
如果不重视程序中那些未被发现的Bug可能带来的后果。这种后果的严重程度可以从一个Bug引起的用户使用不便到系统崩溃。这种后果可能常常会被软件的开发人员所忽视,这种情况会长期损害软件开发商的声誉,并且会对未来的市场产生负面影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个软件开发商获取未来的市场。
很多研究成果表明,无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现得越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早地查找和修改Bug。而单元测试就是一个在早期抓住Bug的机会。
相比后阶段的测试,单元测试的创建更简单,且维护更容易,同时可以更方便地进行重构。从全程的费用来考虑,相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说,单元测试所需的费用是很低的。
误解3:我是个编程高手,无须进行单元测试
在每个开发团队中都至少有一个这样的开发人员,他非常擅长于编程,他开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?在现实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单程序中应付过去,但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。编码不是一个可以一次性通过的过程。在现实世界中,软件产品必须进行维护以对操作需求的改变作出及时响应,并且要对最初的开发工作遗留下来的Bug进行修改。你希望依靠那些原始作者进行修改吗?这些制造出未经测试的代码的资深工程师们还会继续在其他地方制造这样的代码。在开发人员做出修改后进行可重复的单元测试,可以避免产生那些令人不快的负作用。
误解4:测试人员会测出所有Bug
一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种单元调用的测试条件时,要全面考虑单元被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远超过独立进行的单元测试过程。
最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。让我们类比一下,假设我们要清理一台电脑主机中的灰尘,如果没有把主机中各个部件(显卡、内存等)拆开,无论你用什么工具,一些灰尘还会隐藏在主机的某些角落无法清理。但我们换个角度想想,如果把主机每个部件一一拆开,这些死角中的灰尘就容易被发现和接触到了,并且每一部件的灰尘都可以毫不费力地进行清理。
单元测试之症结
测试在软件开发过程中一直都是备受关注的,测试不仅仅局限于软件开发中的一个阶段,它已经开始贯穿于整个软件开发过程。大家普遍认识到,如果测试能在开发阶段进行有效执行,程序的Bug就会被及早发现,其质量就能得到有效的保证,从而减少软件开发总成本。但是,相对于测试这个词的流行程度而言,大家对单元测试的认知普遍存在一些偏差,特别是一些程序员很容易陷入一些误区,导致了测试并没有在他们所在的开发项目中起到有效的作用。下面对一些比较具有代表性的症结进行剖析,并对于测试背后所蕴含的一些设计思考进行阐述,希望能够起到抛砖引玉的作用。
症结1:使用System.out.print跟踪和运行程序就够了
这个症结可以说是程序员的一种通病,认为使用System.out.print就可以确保编写代码的正确性,无须编写测试用例,他们觉得编写用例是在“浪费时间”。使用System.out.print输出结果,以肉眼观察这种刀耕火种的方式进行测试,不仅效率低下,而且容易出错。
症结2:使用System.out.print跟踪和运行程序就够了
在编码的时候,确实存在一些看起来比较难测试的代码,但是并非无法测试。并且在大多数情况下,还是由于被测试的代码在设计时没有考虑到可测试性的问题。编写程序不仅与第三方一些框架耦合过紧,而且过于依赖其运行环境,从而表现出被测试的代码本身很难测试。
症结3:测试代码可以随意写
编写测试代码时抱着一种随意的态度,没有弄清测试的真正意图。编写测试代码只是为了应付任务而已,先编写程序实现代码,然后才去编写一些单元测试。表现出来的结果是测试过于简单,只走形式和花架,将大量Bug传递给系统测试人员。
症结4:不关心测试环境
手工搭建测试环境,测试数据,造成维护困难,占据了大量时间,严重影响效率。对测试产 生的“垃圾”不清除,不处理。造成测试不能重复进行,导致脆弱的测试,需要维护好测试环境,做一个“低碳环保”的测试者。
症结5:测试环境依赖性大
测试环境依赖性大,没有有效隔离测试目标及其依赖环境,一是使测试不聚焦;二是常因依赖环境的影响造成失败;三是因依赖环境太厚重从而降低测试的效率(如依赖数据库或依赖网络资源,如邮件系统、Web服务)。
单元测试基本概念
被测系统:SUT(System Under Test)
被测系统(System under test,SUT)表示正在被测试的系统,目的是测试系统能否正确操作。这一词语常用于软件测试中。软件系统测试的一个特例是对应用软件的测试,称为被测应用程序(application under test,AUT)。
SUT也表明软件已经到了成熟期,因为系统测试在测试周期中是集成测试的后一阶段。
测试替身:Test Double
在单元测试时,使用Test Double减少对被测对象的依赖,使得测试更加单一。同时,让测试案例执行的时间更短,运行更加稳定,同时能对SUT内部的输入输出进行验证,让测试更加彻底深入。但是,Test Double也不是万能的,Test Double不能被过度使用,因为实际交付的产品是使用实际对象的,过度使用Test Double会让测试变得越来越脱离实际。
要理解测试替身,需要了解一下Dummy Objects、Test Stub、Test Spy、Fake Object
这几个概念,下面我们对这些概念分别进行说明。
Dummy Objects
Dummy Objects泛指在测试中必须传入的对象,而传入的这些对象实际上并不会产生任何作用,仅仅是为了能够调用被测对象而必须传入的一个东西。
Test Stub
测试桩是用来接受SUT内部的间接输入(indirect inputs),并返回特定的值给SUT。可以理解Test Stub是在SUT内部打的一个桩,可以按照我们的要求返回特定的内容给SUT,Test Stub的交互完全在SUT内部,因此,它不会返回内容给测试案例,也不会对SUT内部的输入进行验证。
Test Spy
Test Spy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirect outputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,Test Spy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。
Mock Object
Mock Object和Test Spy有类似的地方,它也是安插在SUT内部,获取到SUT内部的间接输出(indirect outputs),不同的是,Mock Object还负责对情报(intelligence)进行验证,总部(外部的测试案例)信任Mock Object的验证结果。
Fake Object
经常,我们会把Fake Object和Test Stub搞混,因为它们都和外部没有交互,对内部的输入输出也不进行验证。不同的是,Fake Object并不关注SUT内部的间接输入(indirect inputs)或间接输出(indirect outputs),它仅仅是用来替代一个实际的对象,并且拥有几乎和实际对象一样的功能,保证SUT能够正常工作。实际对象过分依赖外部环境,Fake Object可以减少这样的依赖。
测试夹具:Test Fixture
所谓测试夹具(Fixture),就是测试运行程序(test runner)会在测试方法之前自动初始化、回收资源的工作。JUnit4之前是通过setUp、TearDown方法完成。在JUnit4中,仍然可以在每个测试方法运行之前初始化字段和配置环境,当然也是通过注解完成。在JUnit4中,通过@Befroe替代setUp方法;@After替代tearDown方法。在一个测试类中,甚至可以使用多个@Before来注解多个方法,这些方法都是在每个测试之前运行。说明一点,@Before是在每个测试方法运行前均初始化一次,同理@Ater是在每个测试方法运行完毕后均执行一次。也就是说,经这两个注解的初始化和注销,可以保证各个测试之间的独立性而互不干扰,它的缺点是效率低。另外,不需要在超类中显式调用初始化和清除方法,只要它们不被覆盖,测试运行程序将根据需要自动调用这些方法。超类中的@Before方法在子类的@Before方法之前调用(与构造函数调用顺序一致),@After方法是子类在超类之前运行。
一个测试用例可以包含若干个打上@Test注解的测试方法,测试用例测试一个或多个类API接口的正确性,当然在调用类API时,需要事先创建这个类的对象及一些关联的对象,这组对象就称为测试夹具(Fixture),相当于测试用例的“工作对象”。
前面讲过,一个测试用例类可以包含多个打上@Test注解的测试方法,在运行时,每个测试方法都对应一个测试用例类的实例。当然,用户可以在具体的测试方法里声明并实例化业务类的实例,在测试完成后销毁它们。但是,这么一来就要在每个测试方法中都重复这些代码,因为TestCase实例依照以下步骤运行。
- 创建测试用例的实例。
- 使用注解@Before注解修饰用于初始化夹具的方法。
- 使用注解@After注解修饰用于注销夹具的方法。
- 保证这两种方法都使用 public void 修饰,而且不能带有任何参数。
TestCase实例运行过程如下图所示:

之所以每个测试方法都需要按以上流程运行,是为了防止测试方法相互之间的影响,因为在同一个测试用例类中不同测试方法可能会使用到相同的测试夹具,前一个测试方法对测试夹具的更改会影响后一个测试方法的现场。而通过如上的运行步骤后,因为每个测试方法运行前都重建运行环境,所以测试方法相互之间就不会有影响了。
可是,这种夹具设置方式还是引来了批评,因为它效率低下,特别是在设置 Fixture 非常耗时的情况下(例如设置数据库链接)。而且对于不会发生变化的测试环境或者测试数据来说,是不会影响到测试方法的执行结果的,也就没有必要针对每一个测试方法重新设置一次夹具。因此在 JUnit 4 中引入了类级别的夹具设置方法,编写规范说明如下。
- 创建测试用例的实例。
- 使用注解BeforeClass 修饰用于初始化夹具的方法。
- 使用注解AfterClass 修饰用于注销夹具的方法。
- 保证这两种方法都使用 public static void 修饰,而且不能带有任何参数。
类级别的夹具仅会在测试类中所有测试方法执行之前执行初始化,并在全部测试方法测试完毕之后执行注销方法,如图16-2所示。
测试用例:Test Case
有了测试夹具,就可以开始编写测试用例的测试方法了。当然也可以不需要测试夹具而直接编写测试用例方法。
在JUnit 3中,测试方法都必须以test为前缀,且必须是public void的,JUnit 4之后,就没有这个限制,只要在每个测试方法标注@Test注解,方法签名可以是任意取名。

可以在一个测试用例中添加多个测试方法,运行器为每个方法生成一个测试用例实例并分别运行。
测试套件:Test Suite
如果每次只能运行一个测试用例,那么又陷入了传统测试(使用main()方法进行测试)的窘境:手工去运行一个个测试用例,这是非常烦琐和低效的,测试套件专门为解决这一问题而来。它通过TestSuite对象将多个测试用例组装成一个测试套件,则测试套件批量运行。需要特别指出的是,可以把一个测试套件整个添加到另一个测试套件中,就像小筐装进大筐里变成一个筐一样。
JUnit4中最显著的特性是没有套件(套件机制用于将测试从逻辑上分组并将这这些测试作为一个单元测试来运行)。为了替代老版本的套件测试,套件被两个新注解代替:@RunWith、@SuteClasses。通过@RunWith指定一个特殊的运行器,即Suite.class套件运行器,并通过@SuiteClasses注解,将需要进行测试的类列表作为参数传入。
创建步骤说明如下:
- 创建一个空类作为测试套件的入口(这个空类必须使用public修饰符,而且存在无参构造函数)。
- 将@RunWith、@SuiteClasses注释修饰这个空类。
- 把Suite.class作为参数传入@RunWith注释,以提示JUnit将此类指定为运行器。
- 将需要测试的类组成数组作为@SuiteClasses的参数。
断言:Assertions
断言(assertion)是测试框架里面的若干个方法,用来判断某个语句的结果是否为真或判断是否与预期相符。比如assertTrue这一方法就是用来判定一条语句或一个表达式的结果是否为真,如果条件为假,那么该断言就会执行失败。
在JUnit 4中一个测试类并不继承自TestCase(在JUnit 3.8中,这个类中定义了assertEquals()方法),所以你必须使用前缀语法(举例来说,Assert.assertEquals())或者静态地导入Assert类。这样我们就可以完全像以前一样使用assertEquals方法。
由于JDK 5.0自动装箱机制的出现,原先的12个assertEquals方法全部去掉了。例如,原先JUnit 3.8中的assertEquals(long,long)方法在JUnit 4中要使用assertEquals(Object,Object),对于assertEquals(byte,byte)、assertEquals(int,int)等也是这样。
在JUnit 4中,新集成了一个assert关键字。你可以像使用assertEquals方法一样来使用它,因为它们都抛出相同的异常(java.lang.AssertionError)。JUnit 3.8的assertEquals将抛出一个junit.framework.AssertionFailedError。注意,当使用assert时,你必须指定Java的"-ea"参数,否则断言将被忽略。
单元测试系列之2:模拟利器Mockito
引述:程序测试对保障应用程序正确性而言,其重要性怎么样强调都不为过。JUnit是必须事先掌握的测试框架,大多数测试框架和测试工具都在此基础上扩展而来,Spring对测试所提供的帮助类也是在JUnit的基础上进行演化的。直接使用JUnit测试基于Spring的应用存在诸多不便,不可避免地需要将大量的精力用于应付测试夹具准备、测试现场恢复、访问测试数据操作结果等边缘性的工作中。Mockito、Unitils、Dbunit等框架的出现,这些问题有了很好的解决方案,特别是Unitils结合Dbunit对测试DAO层提供了强大的支持,大大提高了编写测试用例的效率和质量。
也许在单元测试框架领域,testNG、JUnit的高下上尚有争论,它们各有自己的拥趸,但是在测试模拟领域,Mockito无疑是翘楚,JMock等只能站后了。
模拟测试概述
目前支持Java语言的Mock测试工具有EasyMock、JMock、Mockito、MockCreator、Mockrunner、MockMaker等,Mockito是一个针对Java的Mocking框架。它与EasyMock和JMock很相似,是一套通过简单的方法对于指定的接口或类生成 Mock 对象的类库,避免了手工编写Mock对象。但Mockito是通过在执行后校验什么已经被调用,它消除了对期望行为(Expectations)的需要。使用Mockito,在准备阶段只需花费很少的时间,可以使用简洁的API编写出漂亮的测试,可以对具体的类创建Mock对象,并且有“监视”非Mock对象的能力。
Mockito使用起来简单,学习成本很低,而且具有非常简洁的API,测试代码的可读性很高,因此它十分受欢迎,用户群越来越多,很多开源软件也选择了Mockito。要想了解更多有关Mockito的信息,可以访问其官方网站http://www.mockito.org/。在开始使用Mockito之前,先简单了解一下Stub和Mock的区别。相比Easymock,JMock,编写出来的代码更加容易阅读。无须录制mock方法调用就返回默认值是一个很大优势。目前最新的版本是1.9.0。
Stub对象用来提供测试时所需要的测试数据,可以对各种交互设置相应的回应。例如我们可以设置方法调用的返回值等。Mockito中 when(…).thenReturn(…) 这样的语法便是设置方法调用的返回值。另外也可以设置方法在何时调用会抛出异常等。
Mock对象用来验证测试中所依赖对象间的交互是否能够达到预期。Mockito中用 verify(…).methodXxx(…) 语法来验证 methodXxx方法是否按照预期进行了调用。有关 stub和mock的详细论述请见Martin Fowler的文章《Mocks Aren't Stub》,地址为http://martinfowler.com/articles/mocksArentStubs.html。在Mocking框架中所谓的Mock对象实际上是作为上述的Stub和Mock对象同时使用的。因为它既可以设置方法调用返回值,又可以验证方法的调用。
创建Mock对象
可以对类和接口进行Mock对象的创建,创建的时候可以为Mock对象命名,也可以忽略命名参数。为Mock对象命名的好处就是调试的时候会很方便。比如,我们Mock多个对象,在测试失败的信息中会把有问题的Mock对象打印出来,有了名字我们可以很容易定位和辨认出是哪个Mock对象出现的问题。另外它也有限制,对于final类、匿名类和Java的基本类型是无法进行Mock的。除了用Mock方法来创建模拟对象,如mock(Class<T> classToMock),也可以使用@mock注解定义Mock,下面我们通过实例来介绍一下如何创建一个Mock对象。
- import org.junit.Test;
- import org.mockito.Mock;
- import com.baobaotao.domain.User;
- import com.baobaotao.service.UserService;
- import com.baobaotao.service.UserServiceImpl;
- import static org.junit.Assert.*;
- import static org.mockito.Mockito.*;
- import org.mockito.MockitoAnnotations;
- …
- public class MockitoSampleTest{
- //① 对接口进行模拟
- UserService mockUserService = mock(UserService.class);
- //② 对类进行模拟
- UserServiceImpl mockServiceImpl = mock(UserServiceImpl.class);
- //③ 基于注解模拟类
- @Mock
- User mockUser;
- @Before
- public void initMocks() {
- //④ 初始化当前测试类所有@Mock注解模拟对象
- MockitoAnnotations.initMocks(this);
- }
- …
- }
- …
在①处和②处,通过Mockito提供的mock()方法创建UserService 用户服务接口、用户服务实现类UserServiceImpl的模拟对象。在③处,通过@Mock注解创建用户User类模拟对象,并需要在测试类初始化方法中,通过MockitoAnnotations.initMocks()方法初始化当前测试类中所有打上@Mock注解的模拟对象。如果没有执行这一步初始化动作,测试时会报模拟对象为空对象异常。
设定Mock对象的期望行为及返回值
从上文中我们已经知道可以通过when(mock.someMethod()).thenReturn(value)来设定Mock对象的某个方法调用时的返回值,但它也同样有限制条件:对于static和final修饰的方法是无法进行设定的。下面我们通过实例来介绍一下如何调用方法及设定返回值。
- import org.junit.Test;
- import org.mockito.Mock;
- import com.baobaotao.domain.User;
- import com.baobaotao.service.UserService;
- import com.baobaotao.service.UserServiceImpl;
- …
- public class MockitoSampleTest {
- …
- //① 模拟接口UserService测试
- @Test
- public void testMockInterface() {
- //①-1 对方法设定返回值
- when(mockUserService.findUserByUserName("tom")).thenReturn(
- new User("tom", "1234"));
- //①-2 对方法设定返回值
- doReturn(true).when(mockServiceImpl).hasMatchUser("tom", "1234");
- //①-3 对void方法进行方法预期设定
- User u = new User("John", "1234");
- doNothing().when(mockUserService).registerUser(u);
- //①-4 执行方法调用
- User user = mockUserService.findUserByUserName("tom");
- boolean isMatch = mockUserService.hasMatchUser("tom","1234");
- mockUserService.registerUser(u);
- assertNotNull(user);
- assertEquals(user.getUserName(), "tom");
- assertEquals(isMatch, true);
- }
- //② 模拟实现类UserServiceImpl测试
- @Test
- public void testMockClass() {
- // 对方法设定返回值
- when(mockServiceImpl.findUserByUserName("tom"))
- .thenReturn(new User("tom", "1234"));
- doReturn(true).when(mockServiceImpl).hasMatchUser("tom", "1234");
- User user = mockServiceImpl.findUserByUserName("tom");
- boolean isMatch = mockServiceImpl.hasMatchUser("tom","1234");
- assertNotNull(user);
- assertEquals(user.getUserName(), "tom");
- assertEquals(isMatch, true);
- }
- //③ 模拟User类测试
- @Test
- public void testMockUser() {
- when(mockUser.getUserId()).thenReturn(1);
- when(mockUser.getUserName()).thenReturn("tom");
- assertEquals(mockUser.getUserId(),1);
- assertEquals(mockUser.getUserName(), "tom");
- }
- …
在①处,模拟测试接口UserService的findUserByUserName()方法、hasMatchUser()方法及registerUser()方法。在①-1处通过when().thenReturn()语法,模拟方法调用及设置方法的返回值,实例通过模拟调用UserService 用户服务接口的查找用户findUserByUserName()方法,查询用户名为“tom”详细的信息,并设置返回User对象:new User("tom", "1234")。在①-2处通过doReturn (). when ()语法,模拟判断用户hasMatchUser()方法的调用,判断用户名为“tom”及密码为“1234”的用户存在,并设置返回值为:true。在①-3处对void方法进行方法预期设定,如实例中调用注册用户registerUser()方法。设定调用方法及返回值之后,就可以执行接口方法调用验证。在②处和③处,模拟测试用户服务实现类UserServiceImpl,测试的方法与模拟接口一致。
验证交互行为
Mock对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择地对其交互行为进行验证。在Mockito中验证mock对象交互行为的方法是verify(mock). xxx()。于是用此方法验证了findUserByUserName()方法的调用,因为只调用了一次,所以在verify中我们指定了times参数或atLeastOnce()参数。最后验证返回值是否和预期一样。
- import org.junit.Test;
- import org.mockito.Mock;
- import com.baobaotao.domain.User;
- import com.baobaotao.service.UserService;
- import com.baobaotao.service.UserServiceImpl;
- …
- public class MockitoSampleTest {
- …
- //① 模拟接口UserService测试
- @Test
- public void testMockInterface() {
- …
- when(mockUserService.findUserByUserName("tom"))
- .thenReturn(new User("tom", "1234"));
- User user = mockServiceImpl.findUserByUserName("tom");
- //①-4 验证返回值
- assertNotNull(user);
- assertEquals(user.getUserName(), "tom");
- assertEquals(isMatch, true);
- //①-5 验证交互行为
- verify(mockUserService).findUserByUserName("tom");
- //①-6 验证方法至少调用一次
- verify(mockUserService, atLeastOnce()).findUserByUserName("tom");
- verify(mockUserService, atLeast(1)).findUserByUserName("tom");
- //①-7 验证方法至多调用一次
- verify(mockUserService, atMost(1)).findUserByUserName("tom");
- }
- …
Mockio为我们提供了丰富调用方法次数的验证机制,如被调用了特定次数verify(xxx, times(x))、至少x次verify(xxx, atLeast (x))、最多x次verify(xxx, atMost (x))、从未被调用verify(xxx, never())。在①-6处,验证findUserByUserName()方法至少被调用一次。在①-7处,验证findUserByUserName()方法至多被调用一次.
单元测试系列之3:测试整合之王Unitils
引述:程序测试对保障应用程序正确性而言,其重要性怎么样强调都不为过。JUnit是必须事先掌握的测试框架,大多数测试框架和测试工具都在此基础上扩展而来,Spring对测试所提供的帮助类也是在JUnit的基础上进行演化的。直接使用JUnit测试基于Spring的应用存在诸多不便,不可避免地需要将大量的精力用于应付测试夹具准备、测试现场恢复、访问测试数据操作结果等边缘性的工作中。Mockito、Unitils、Dbunit等框架的出现,这些问题有了很好的解决方案,特别是Unitils结合Dbunit对测试DAO层提供了强大的支持,大大提高了编写测试用例的效率和质量。
Unitils海纳百川(Junit,dbunit,mockito spring hibernate and so on..),以打造一个在实际应用开发中真正实战的测试框架,是致力于应用实战的开发者不得不学习的开源框架。
Unitils概述
Unitils测试框架目的是让单元测试变得更加容易和可维护。Unitils构建在DbUnit与EasyMock项目之上并与JUnit和TestNG相结合。支持数据库测试,支持利用Mock对象进行测试并提供与Spring和Hibernate相集成。Unitils设计成以一种高度可配置和松散耦合的方式来添加这些服务到单元测试中,目前其最新版本是3.1。
Unitils功能特点
- 自动维护和强制关闭单元测试数据库(支持Oracle、Hsqldb、MySQL、DB2)。
- 简化单元测试数据库连接的设置。
- 简化利用DbUnit测试数据的插入。
- 简化Hibernate session管理。
- 自动测试与数据库相映射的Hibernate映射对象。
- 易于把Spring管理的Bean注入到单元测试中,支持在单元测试中使用Spring容器中的Hibernate SessionFactory。
- 简化EasyMock Mock对象创建。
- 简化Mock对象注入,利用反射等式匹配EasyMock参数。
Unitils模块组件
Unitils通过模块化的方式来组织各个功能模块,采用类似于Spring的模块划分方式,如unitils-core、unitils-database、unitils-mock等。比以前整合在一个工程里面显得更加清晰,目前所有模块如下所示:
- unitils-core:核心内核包。
- unitils-database:维护测试数据库及连接池。
- unitils-DbUnit:使用DbUnit来管理测试数据。
- unitils-easymock:支持创建Mock和宽松的反射参数匹配。
- unitils-inject:支持在一个对象中注入另一个对象。
- unitils-mock:整合各种Mock,在Mock的使用语法上进行了简化。
- unitils-orm:支持Hibernate、JPA的配置和自动数据库映射检查。
- unitils-spring:支持加载Spring的上下文配置,并检索和Spring Bean注入。
Unitils的核心架构中包含Moudule和TestListener两个概念,类似Spring中黏连其他开源软件中的FactoryBean概念。可以看成第三方测试工具的一个黏合剂。整体框架如图16-4所示:

通过TestListener可以在测试运行的不同阶段注入某些功能。同时某一个TestListener又被一个对应的Module所持有。Unitils也可以看成一个插件体系结构,TestListener在整个Unitils中又充当了插件中扩展点的角色,从TestListener这个接口中我们可以看到,它可以在crateTestObject、before(after)Class、before(after)TestMethod、beforeSetup、afterTeardown的不同切入点添加不同的动作。
Unitils配置文件
- unitils-defaults.properties:默认配置文件,开启所有功能。
- unitils.properties:项目级配置文件,用于项目通用属性配置。
- unitils-local.properties:用户级配置文件,用于个人特殊属性配置。
Unitils的配置定义了一般配置文件的名字unitils.properties和用户自定义配置文件unitils-local.properties,并给出了默认的模块及模块对应的className,便于Unitils加载对应的模块module。但是如果用户分别在unitils.properties文件及unitils -local.properties文件中对相同属性配置不同值时,将会以unitils-local.properties 的配置内容为主。
Unitils断言
典型的单元测试一般都包含一个重要的组成部分:对比实际产生的结果和希望的结果是否一致的方法,即断言方法(assertEquals)。Unitils 为我们提供了一个非常实用的断言方法,我们以第2章中编写的用户领域对象User为蓝本,比较两个User对象的实例来开始认识Unitils的断言之旅。
assertReflectionEquals:反射断言
在Java世界中,要比较现有两个对象实例是否相等,如果类没有重写equals()方法,用两个对象的引用是否一致作为判断依据。有时候,我们并不需要关注两个对象是否引用同一个对象,只要两个对象的属性值一样就可以了。在JUnit单元测试中,有两种测试方式进行这样的场景测试:一是在比较实体类中重写equals()方法,然后进行对象比较;二是把对象实例的属性一个一个进行比较。不管采用哪种方法,都比较烦锁,Unitils为我们提供了一种非常简单的方法,即使用ReflectionAssert.assertReflectionEquals方法, 如代码清单16-11所示:
- package com.baobaotao.test;
- import java.util.*;
- import org.junit.Test;
- import static org.unitils.reflectionassert.ReflectionAssert.*;
- import static org.unitils.reflectionassert.ReflectionComparatorMode.*;
- import com.baobaotao.domain.User;
- public class AssertReflectionEqualsTest {
- @Test
- public void testReflection(){
- User user1 = new User("tom","1234");
- User user2 = new User("tom","1234");
- ReflectionAssert.assertReflectionEquals(user1, user2);
- }
- }
ReflectionAssert. AssertReflectionEquals(期望值,实际值,比较级别)方法为我们提供了各种级别的比较断言。下面我们依次介绍这些级别的比较断言。
ReflectionComparatorMode.LENIENT_ORDER:忽略要断言集合collection 或者array 中元素的顺序。
ReflectionComparatorMode.IGNORE_DEFAULTS:忽略Java类型默认值,如引用类型为null,整型类型为0,或者布尔类型为false时,那么断言忽略这些值的比较。
ReflectionComparatorMode.LENIENT_DATES:比较两个实例的Date是不是都被设置了值或者都为null,而忽略Date的值是否相等。
assertLenientEquals:断言
ReflectionAssert 类为我们提供了两种比较断言:既忽略顺序又忽略默认值的断言assertLenientEquals,使用这种断言就可以进行简单比较。下面通过实例学习其具体的用法,如代码清单16-12所示。
- package com.baobaotao.test;
- import java.util.*;
- …
- public class AssertReflectionEqualsTest {
- Integer orderList1[] = new Integer[]{1,2,3};
- Integer orderList2[] = new Integer[]{3,2,1};
- //① 测试两个数组的值是否相等,忽略顺序
- //assertReflectionEquals(orderList1, orderList2,LENIENT_ORDER);
- assertLenientEquals(orderList1, orderList2);
- //② 测试两个对象的值是否相等,忽略时间值是否相等
- User user1 = new User("tom","1234");
- Calendar cal1 = Calendar.getInstance();
- user1.setLastVisit(cal1.getTime());
- User user2 = new User("tom","1234");
- Calendar cal2 = Calendar.getInstance();
- cal2.set(Calendar.DATE, 15);
- user2.setLastVisit(cal2.getTime());
- //assertReflectionEquals(user1, user2,LENIENT_DATES);
- assertLenientEquals(user1, user2);
- }
assertPropertyXxxEquals:属性断言
assertLenientEquals 和assertReflectionEquals 这两个方法是把对象作为整体进行比较,ReflectionAssert 类还给我们提供了只比较对象特定属性的方法:assertPropertyReflection Equals()和assertPropertyLenientEquals()。下面通过实例学习其具体的用法,如代码清单16-13所示。
- package com.baobaotao.test;
- import java.util.*;
- …
- public class AssertReflectionEqualsTest {
- User user = new User("tom","1234");
- assertPropertyReflectionEquals("userName", "tom", user);
- assertPropertyLenientEquals("lastVisit", null, user);
- }
assertPropertyReflectionEquals()断言是默认严格比较模式但是可以手动设置比较级别的断言,assertPropertyLenientEquals()断言是具有忽略顺序和忽略默认值的断言。
集成Spring
Unitils 提供了一些在Spring 框架下进行单元测试的特性。Spring 的一个基本特性就是,类要设计成为没有Spring 容器或者在其他容器下仍然易于进行单元测试。但是很多时候在Spring 容器下进行测试还是非常有用的。
Unitils 提供了以下支持 Spring 的特性:
- ApplicationContext 配置的管理;
- 在单元测试代码中注入Spring 的Beans;
- 使用定义在Spring 配置文件里的Hibernate SessionFactory;
- 引用在Spring 配置中Unitils 数据源。
ApplicationContext 配置
可以简单地在一个类、方法或者属性上加上@SpringApplicationContext 注解,并用Spring的配置文件作为参数,来加载Spring应用程序上下文。下面我们通过实例来介绍一下如何创建ApplicationContext。
- import org.junit.Test;
- import org.springframework.context.ApplicationContext;
- import org.unitils.UnitilsJUnit4;
- import org.unitils.spring.annotation.SpringApplicationContext;
- import org.unitils.spring.annotation.SpringBean;
- import com.baobaotao.service.UserService;
- import static org.junit.Assert.*;
- //①用户服务测试
- public class UserServiceTest extends UnitilsJUnit4 {
- //①-1 加载Spring配置文件
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"})
- private ApplicationContext applicationContext;
- //①-1 加载Spring容器中的Bean
- @SpringBean("userService")
- private UserService userService;
- //①-3 测试Spring容器中的用户服务Bean
- @Test
- public void testUserService (){
- assertNotNull(applicationContext);
- assertNotNull(userService.findUserByUserName("tom"));
- }
- }
- …
在①-1处,通过@SpringApplicationContext 注解加载baobaotao-service.xml和baobaotao- dao.xml两个配置文件,生成一个Spring应用上下文,我们就可以在注解的范围内引用applicationContext这个上下文。在①-2处,通过@SpringBean注解注入当前Spring容器中相应的Bean,如实例中加载ID为“userService”的Bean到当前测试范围。在①-3处,通过JUnit断言验证是否成功加载applicationContext和userService。Unitils加载Spring上下文的过程是:首先扫描父类的@SpringApplicationContext注解,如果找到了就在加载子类的配置文件之前加载父类的配置文件,这样就可以让子类重写配置文件和加载特定配置文件。
细心的读者可能会发现,采用这种方式加载Spring应用上下文,每次执行测试时,都会重复加载Spring应用上下文。Unitils为我们提供在类上加载Spring应用上下文的能力,以避免重复加载的问题。
- …
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"})
- public class BaseServiceTest extends UnitilsJUnit4 {
- //加载Spring上下文
- @SpringApplicationContext
- public ApplicationContext applicationContext;
- }
在父类BaseServiceTest里指定了Spring配置文件,Spring应用上下文只会创建一次,然后在子类SimpleUserServiceTest 里会重用这个应用程序上下文。加载Spring应用上下文是一个非常繁重的操作,如果重用这个Spring应用上下文就会大大提升测试的性能。
- …
- public class SimpleUserServiceTest extends BaseServiceTest {
- //① Spring容器中加载Id为"userService"的Bean
- @SpringBean("userService")
- private UserService userService1;
- //② 从Spring容器中加载与UserService相同类型的Bean
- @SpringBeanByType
- private UserService userService2;
- //③ 从Spring容器中加载与userService相同名称的Bean
- @SpringBeanByName
- private UserService userService;
- //④ 使用父类的Spring上下文
- @Test
- public void testApplicationContext(){
- assertNotNull(applicationContext);
- }
- @Test
- public void testUserService(){
- assertNotNull(userService.findUserByUserName("tom"));
- assertNotNull(userService1.findUserByUserName("tom"));
- assertNotNull(userService2.findUserByUserName("tom"));
- }
- }
- …
在①处,使用@SpringBean 注解从Spring容器中加载一个ID为userService的Bean。在②处,使用@ SpringBeanByType注解从Spring容器中加载一个与UserService相同类型的Bean,如果找不到相同类型的Bean,就会抛出异常。在③处,使用@SpringBeanByName 注解从Spring容器中加载一个与当前属性名称相同的Bean。
集成Hibernate
Hibernate是一个优秀的O / R开源框架,它极大地简化了应用程序的数据访问层开发。虽然我们在使用一个优秀的O/R框架,但并不意味我们无须对数据访问层进行单元测试。单元测试仍然非常重要。它不仅可以确保Hibernate映射类的映射正确性,也可以很便捷地测试HQL查询等语句。Unitils为方便测试 Hibernate,提供了许多实用的工具类,如HibernateUnitils就是其中一个,使用assertMappingWithDatabaseConsistent()方法,就可以方便测试映射文件的正确性。
SessionFactory 配置
可以简单地在一个类、方法或者属性上加上@ HibernateSessionFactory 注解,并用Hibernate的配置文件作为参数,来加载Hibernate上下文。下面我们通过实例来介绍一下如何创建SessionFactory。
- …
- @HibernateSessionFactory("hibernate.cfg.xml")
- public class BaseDaoTest extends UnitilsJUnit4 {
- @HibernateSessionFactory
- public SessionFactory sessionFactory;
- @Test
- public void testSessionFactory(){
- assertNotNull(sessionFactory);
- }
- }
在父类BaseDaoTest里指定了Hibernate配置文件,Hibernate应用上下文只会创建一次,然后在子类SimpleUserDaoTest里会重用这个应用程序上下文。加载Hibernate应用上下文是一个非常繁重的操作,如果重用这个Hibernate应用上下文就会大大提升测试的性能。
- …
- public class SimpleUserDaoTest extends BaseDaoTest {
- private UserDao userDao;
- //① 初始化UserDao
- @Before
- public void init(){
- userDao = new WithoutSpringUserDaoImpl();
- userDao.setSessionFactory(sessionFactory); //使用父类的SessionFactory
- }
- //② Hibernate映射测试
- @Test
- public void testMappingToDatabase() {
- HibernateUnitils.assertMappingWithDatabaseConsistent();
- }
- //③ 测试UserDao
- @Test
- public void testUserDao(){
- assertNotNull(userDao);
- assertNotNull(userDao.findUserByUserName("tom"));
- assertEquals("tom", userDao.findUserByUserName("tom").getUserName());
- }
- }
- …
为了更好演示如何应用Unitils测试基于Hibernate数据访问层,在这个实例中不使用Spring框架。所以在执行测试时,需要先创建相应的数据访问层实例,如实例中的userDao。其创建过程如①处所示,先手工实例化一个UserDao,然后获取父类中创建的SessionFactory,并设置到UserDao中。在②处,使用Unitils提供的工具类HibernateUnitils中的方法测试我们的Hibernate映射文件。在③处,通过JUnit的断言验证 UserDao相关方法,看是否与我们预期的结果一致。
集成Dbunit
Dbunit是一个基于JUnit扩展的数据库测试框架。它提供了大量的类,对数据库相关的操作进行了抽象和封装。Dbunit通过使用用户自定义的数据集以及相关操作使数据库处于一种可知的状态,从而使得测试自动化、可重复和相对独立。虽然不用Dbunit也可以达到这种目的,但是我们必须为此付出代价(编写大量代码、测试及维护)。既然有了这么优秀的开源框架,我们又何必再造轮子。目前其最新的版本是2.4.8。
随着Unitils的出现,将Spring、Hibernate、DbUnit等整合在一起,使得DAO层的单元测试变得非常容易。Unitils采用模块化方式来整合第三方框架,通过实现扩展模块接口org.unitils.core.Module来实现扩展功能。在Unitils中已经实现一个DbUnitModule,很好整合了DbUnit。通过这个扩展模块,就可以在Unitils中使用Dbunit强大的数据集功能,如用于准备数据的@DataSet注解、用于验证数据的@ExpectedDataSet注解。Unitils集成DbUnit流程图如图16-5所示。

16.4.5 自定义扩展模块
Unitils通过模块化的方式来组织各个功能模块,对外提供一个统一的扩展模块接口org.unitils.core.Module来实现与第三方框架的集成及自定义扩展。在Unitils中已经实现目前一些主流框架的模块扩展,如Spring、Hibernate、DbUnit、Testng等。如果这些内置的扩展模块无法满足需求,我们可以实现自己的一些扩展模块。扩展Unitils模块很简单,如代码清单16-19所示。
- package sample.unitils.module;
- import java.lang.reflect.Method;
- import org.unitils.core.TestListener;
- import org.unitils.core. Module;
- //① 实现Module接口
- public class CustomExtModule implements Module {
- //② 实现获取测试监听的方法
- public TestListener getTestListener() {
- return new CustomExtListener();
- }
- //② 新建监听模块
- protected class CustomExtListener extends TestListener {
- //③ 重写 TestListener里的相关方法,完成相关扩展的功能
- @Override
- public void afterTestMethod(Object testObject, Method testMethod,
- Throwable testThrowable) {
- …
- }
- @Override
- public void beforeTestMethod(Object testObject, Method testMethod) {
- …
- }
- }
- …
- }
在①处新建自定义扩展模块CustomExtModule,实现Module接口。在②处新建自定义监听模块,继承TestListener。在③处重写(@Override)TestListener里的相关方法,完成相关扩展的功能。实现自定义扩展模块之后,剩下的工作就是在Unitils配置文件unitils.properties中注册这个自定义扩展的模块:
unitils.module. custom.className= sample.unitils.module.CustomExtModule
单元测试系列之4:使用Unitils测试DAO层
Spring 的测试框架为我们提供一个强大的测试环境,解决日常单元测试中遇到的大部分测试难题:如运行多个测试用例和测试方法时,Spring上下文只需创建一次;数据库现场不受破坏;方便手工指定Spring配置文件、手工设定Spring容器是否需要重新加载等。但也存在不足的地方,基本上所有的Java应用都涉及数据库,带数据库应用系统的测试难点在于数据库测试数据的准备、维护、验证及清理。Spring 测试框架并不能很好地解决所有问题。要解决这些问题,必须整合多方资源,如DbUnit、Unitils、Mokito等。其中Unitils正是这样的一个测试框架。数据库测试的难点
按照Kent Back的观点,单元测试最重要的特性之一应该是可重复性。不可重复的单元测试是没有价值的。因此好的单元测试应该具备独立性和可重复性,对于业务逻辑层,可以通过Mockito底层对象和上层对象来获得这种独立性和可重复性。而DAO层因为是和数据库打交道的层,其单元测试依赖于数据库中的数据。要实现DAO层单元测试的可重复性就需要对每次因单元测试引起数据库中的数据变化进行还原,也就是保护单元测试数据库的数据现场。
扩展Dbunit用Excel准备数据
在测试数据访问层(DAO)时,通常需要经过测试数据的准备、维护、验证及清理的过程。这个过程不仅烦锁,而且容易出错,如数据库现场容易遭受破坏、如何对数据操作正确性进行检查等。虽然Spring测试框架在这一方面为我们减轻了很多工作,如通过事务回滚机制来保存数据库现场等,但对测试数据及验证数据准备方面还没有一种很好的处理方式。Unitils框架出现,改变了难测试DAO的局面,它将SpringModule、DatabaseModule、DbUnitModule等整合在一起,使得DAO的单元测试变得非常容易。基于Unitils框架的DAO测试过程如图16-6所示。

以JUnit作为整个测试的基础框架,并采用DbUnit作为自动管理数据库的工具,以XML、Excel作为测试数据及验证数据准备,最后通过Unitils的数据集注解从Excel、XML文件中加载测试数据。使用一个注解标签就可以完成加载、删除数据操作。由于XML作为数据集易用性不如Excel,在这里就不对XML数据集进行讲解。下面我们主要讲解如何应用Excel作为准备及验证数据的载体,减化DAO单元测试。由于Unitils没有提供访问Excel的数据集工厂,因此需要编写插件支持Excel格式数据源。Unitils提供一个访问XML的数据集工厂MultiSchemaXmlDataSetFactory,其继承自DbUnit提供的数据集工厂接口DataSetFactory。我们可以参考这个XML数据集工厂类,编写一个访问Excel的数据集工厂MultiSchemaXlsDataSetFactory及Excel数据集读取器MultiSchemaXlsDataSetReader,然后在数据集读取器中调用Apache POI类库来读写Excel文件。如代码清单16-20所示。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- public class MultiSchemaXlsDataSetFactory implements DataSetFactory {
- protected String defaultSchemaName;
- //① 初始化数据集工厂
- public void init(Properties configuration, String defaultSchemaName) {
- this.defaultSchemaName = defaultSchemaName;
- }
- //② 从Excel文件创建数据集
- public MultiSchemaDataSet createDataSet(File... dataSetFiles) {
- try {
- MultiSchemaXlsDataSetReader xlsDataSetReader =
- new MultiSchemaXlsDataSetReader(defaultSchemaName);
- return xlsDataSetReader.readDataSetXls(dataSetFiles);
- } catch (Exception e) {
- throw new UnitilsException("创建数据集失败: "
- + Arrays.toString(dataSetFiles), e);
- }
- }
- //③ 获取数据集文件的扩展名
- public String getDataSetFileExtension() {
- return "xls";
- }
- }
- …
与XML数据集工厂MultiSchemaXmlDataSetFactory一样,Excel的数据集工厂也需要实现数据集工厂接口DataSetFactory的三个方法:init(…)、createDataSet(File... dataSetFiles)、getDataSetFileExtension()。在①处,初始化数据集工厂,需要设置一个默认的数据库表模式名称defaultSchemaName。在②处,执行创建多数据集,具体读取构建数据集的过程封装在Excel读取器MultiSchemaXlsDataSetReader中。在③处,获取数据集文件的扩展名,对Excel文件而言就是“xls”。下面来看一下这个数据集读取器的实现代码。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- // Excel数据集读取器
- public class MultiSchemaXlsDataSetReader {
- private String defaultSchemaName;
- public MultiSchemaXlsDataSetReader(String defaultSchemaName) {
- this.defaultSchemaName = defaultSchemaName;
- }
- // Excel数据集读取器
- public MultiSchemaDataSet readDataSetXls(File... dataSetFiles) {
- try {
- Map<String, List<ITable>> tableMap = getTables(dataSetFiles);
- MultiSchemaDataSet dataSets = new MultiSchemaDataSet();
- for (Entry<String, List<ITable>> entry : tableMap.entrySet()) {
- List<ITable> tables = entry.getValue();
- try {
- DefaultDataSet ds = new DefaultDataSet(tables
- .toArray(new ITable[] {}));
- dataSets.setDataSetForSchema(entry.getKey(), ds);
- } catch (AmbiguousTableNameException e) {
- throw new UnitilsException("构造DataSet失败!", e);
- }
- }
- return dataSets;
- } catch (Exception e) {
- throw new UnitilsException("解析EXCEL文件出错:", e);
- }
- }
- …
- }
- …
根据传入的多个Excel文件,构造一个多数据集。 其中一个数据集对应一个Excel文件,一个Excel的Sheet表对应一个数据库Table。通过DbUnit提供Excel数据集构造类XlsDataSet,可以很容易将一个Excel文件转换为一个数据集:XlsDataSet(new FileInputStream(xlsFile))。最后将得到的多个DataSet用MultiSchemaDataSet进行封装。
下面就以一个用户DAO的实现类WithoutSpringUserDaoImpl为例,介绍如何使用我们实现的Excel数据集工厂。为了让Unitils使用自定义的数据集工厂,需要在unitils.properties配置文件中指定自定义的数据集工厂。
DbUnitModule.DataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
其中DbUnitModule.DataSet.factory.default是配置数据集工厂类,在测试方法中可以使用@DataSet注解加载指定的准备数据。默认是XML数据集工厂,这里指定自定义数据集工厂类全限定名为 sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory。
其中DbUnitModule. ExpectedDataSet.factory.default是配置验证数据集工厂类,也是指定自定义数据集工厂类,使用@ ExpectedDataSet注解加载验证数据。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- public class UserDaoTest extends UnitilsJUnit4 {
- @Test
- @DataSet //① 准备测试数据
- public void getUser() {
- …
- }
- @Test
- @DataSet("BaobaoTao.SaveUser.xls") //② 准备测试数据 -
- @ExpectedDataSet //③ 准备验证数据
- public void saveUser()throws Exception {
- …
- }
- }
- …
@DateSet 注解表示了测试时需要寻找DbUnit的数据集文件进行加载,如果没有指明数据集的文件名,则Unitils自动在当前测试用例所在类路径下加载文件名为测试用例类名的数据集文件,实例中①处,将到UserDaoTest.class所在目录加载WithExcelUserDaoTest.xls 数据集文件。
@ExpectedDataSet注解用于加载验证数据集文件,如果没有指明数据集的文件名,则会在当前测试用例所在类路径下加载文件名为testClassName.methodName-result.xls的数据集文件。实例中③处将加载UserDaoTest. saveUser.result.xls数据集文件。
测试实战
使用JUnit作为基础测试框架,结合Unitils、DbUnit管理测试数据,并使用我们编写的Excel数据集工厂(见代码清单16 20)。从Excel数据集文件中获取准备数据及验证数据,并使用HSQLDB作为测试数据库。下面详细介绍如何应用Excel准备数据集及验证数据集来测试DAO。
在进行DAO层的测试之前,我们先来认识一下需要测试的UserDaoImpl用户数据访问类。UserDaoImpl用户数据访问类中拥有一个获取用户信息和保存注册用户信息的方法,其代码如下所示。
- import java.util.List;
- import org.hibernate.Session;
- import org.hibernate.SessionFactory;
- import org.springframework.orm.hibernate3.HibernateTemplate;
- import com.baobaotao.dao.UserDao;
- import com.baobaotao.domain.User;
- public class UserDaoImpl implements UserDao {
- //通过用户名获取用户信息
- public User findUserByUserName(String userName) {
- String hql = " from User u where u.userName=?";
- List<User> users = getHibernateTemplate().find(hql, userName);
- if (users != null && users.size() > 0)
- return users.get(0);
- else
- return null;
- }
- //保存用户信息
- public void save(User user) {
- getHibernateTemplate().saveOrUpdate(user);
- }
- …
- }
我们认识了需要测试的UserDaoImpl用户数据访问类之后,还需要认识一下用于表示用户领域的对象User,在演示测试保存用户信息及获取用户信息时需要用到此领域对象,其代码如下所示。
- import javax.persistence.Column;
- import javax.persistence.Entity;
- …
- @Entity
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Table(name = "t_user")
- public class User implements Serializable{
- @Id
- @Column(name = "user_id")
- protected int userId;
- @Column(name = "user_name")
- protected String userName;
- protected String password;
- @Column(name = "last_visit")
- protected Date lastVisit;
- @Column(name = "last_ip")
- protected String lastIp;
- @Column(name = "credits")
- private int credits;
- …
- }
用户登录日志领域对象LoginLog与用户领域对象Hibernate注解配置一致,这里就不再列出,读者可以参考本书附带光盘中的实例代码。在实例测试中,我们直接使用Hibernate进行持久化操作,所以还需要对Hibernate进行相应配置,详细的配置清单如下所示。
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE hibernate-configuration PUBLIC
- "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
- "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
- <hibernate-configuration>
- <session-factory>
- <!--① SQL方言,这边设定的是HSQL -->
- <property name="dialect">org.hibernate.dialect.HSQLDialect</property>
- <!--② 数据库连接配置 -->
- <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
- <property name="hibernate.connection.url">
- jdbc:hsqldb:data/sampledb
- </property>
- <!--设置连接数据库的用户名-->
- <property name="hibernate.connection.username">sa</property>
- <!--设置连接数据库的密码-->
- <property name="hibernate.connection.password"></property>
- <!--③ 设置显示sql语句方便调试-->
- <property name="hibernate.show_sql">true</property>
- <!--④ 配置映射 -->
- <property name="configurationClass">
- org.hibernate.cfg.AnnotationConfiguration
- </property>
- <mapping class="com.baobaotao.domain.User" />
- <mapping class="com.baobaotao.domain.LoginLog" />
- </session-factory>
- </hibernate-configuration>
选用HSQLDB作为测试数据库,在①处,配置HSQLDB的SQL方言HSQLDialect。在②处,对连接数据库驱动及数据库连接进行相应的配置。为了方便测试调试,在③处设置显示Hibernate生成的SQL语句。在④处启用Hibernate的注解功能,并配置相应的领域对象,如实例中的User、LoginLog。将配置好的hibernate.cfg.xml放在src目录下。
配置Unitils测试环境
要在单元测试中更好地使用Unitils ,首先需要在测试源码的根目录中创建一个项目级unitils.properties 配置文件,实例中unitils.properties详细配置清单如下所示。
- #① 启用unitils所需模块
- unitils.modules=database,dbunit,hibernate,spring
- #自定义扩展模块,详见实例源码
- unitils.module.dbunit.className=sample.unitils.module.CustomExtModule
- #② 配置数据库连接
- database.driverClassName=org.hsqldb.jdbcDriver
- database.url=jdbc:hsqldb:data/sampledb;shutdown=true
- database.userName=sa
- database.password=
- database.schemaNames=public
- database.dialect = hsqldb
- #③ 配置数据库维护策略.
- updateDataBaseSchema.enabled=true
- #④ 配置数据库表创建策略
- dbMaintainer.autoCreateExecutedScriptsTable=true
- dbMaintainer.script.locations=D:/masterSpring/chapter16/resources/dbscripts
- #⑤ 数据集加载策略
- #DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
- #⑥ 配置数据集工厂
- DbUnitModule.DataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
- DbUnitModule.ExpectedDataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
- #⑦ 配置事务策略
- DatabaseModule.Transactional.value.default=commit
- #⑧ 配置数据集结构模式XSD生成路径
- dataSetStructureGenerator.xsd.dirName=resources/xsd
我们知道unitils.properties中配置的属性是整个项目级别的,整个项目都可以使用这些全局的属性配置。特定用户使用的属性可以设置在unitils-local.properties 文件中,比如user、password和schema,这样每个开发者就使用自定义的测试数据库的schema,而且彼此之间也不会产生影响,实例的详细配置清单如下所示。
- …
- database.userName=sa
- database.password=
- database.schemaNames=public
- …
如果用户分别在unitils.properties文件及unitils -local.properties文件中对相同属性配置不同值时,将会以unitils-local.properties 配置内容为主。如在unitils.properties配置文件中,也配置了database.schemaNames=xxx,测试时启用的是用户自定义配置中的值database.schemaNames=public。
默认的数据集加载机制采用先清理后插入的策略,也就是数据在被写入数据库的时候是会先删除数据集中有对应表的数据,然后将数据集中的数据写入数据库。这个加载策略是可配置的,我们可以通过修改DbUnitModule.DataSet.loadStrategy.default的属性值来改变加载策略。如实例代码清单16 27中⑤配置策略,这时加载策略就由先清理后插入变成了插入,数据已经存在表中将不会被删除,测试数据只是进行插入操作。可选的加载策略列表如下所示。
- CleanInsertLoadStrategy:先删除dateSet中有关表的数据,然后再插入数据。
- InsertLoadStrategy:只插入数据。
- RefreshLoadStrategy:有同样key的数据更新,没有的插入。
- UpdateLoadStrategy: 有同样key的数据更新,没有的不做任何操作。
配置事务策略
在测试DAO的时候都会填写一些测试数据,每个测试运行都会修改或者更新了数据,当下一个测试运行的时候,都需要将数据恢复到原有状态。如果使用的是Hibernate或者JPA,需要每个测试都运行在事务中,保证系统的正常工作。默认情况下,事务管理是disabled的,我们可以通过修改DatabaseModule.Transactional.value.default配置选项,如实例代码清单16 27中⑧配置策略,这时每个测试都将执行commit,其他可选的配置属性值有rollback和disabled。
准备测试数据库及测试数据
配置好了Unitils基本配置、加载模块、数据集创建策略、事务策略之后,我们就着手开始测试数据库及测试数据准备工作,首先我们创建测试数据库。
创建测试数据库
在源码包根目录下创建一个dbscripts文件夹(文件夹目录结构如图16-7所示),且这个文件夹必须与在unitils.properties 文件中dbMaintainer.script.locations配置项指定的位置一致,如代码清单16 27中④ 所示。

在这个文件夹中创建一个数据库创建脚本文件001_create_sampledb.sql,里面包含创建用户表t_user 及登录日志表t_login_log,详细的脚本如下所示。
- CREATE TABLE t_user (
- user_id INT generated by default as identity (start with 100),
- user_name VARCHAR(30),credits INT,
- password VARCHAR(32),last_visit timestamp,
- last_ip VARCHAR(23), primary key (user_id));
- CREATE TABLE t_login_log (
- login_log_id INT generated by default as identity (start with 1),
- user_id INT,
- ip VARCHAR(23),
- login_datetime timestamp,
- primary key (login_log_id));
细心的读者可能会发现这个数据库创建脚本文件名好像存在一定的规则,是的,这个脚本文件命名需要按以下规则命名:版本号 + “_” + “自定义名称” + “ .sql” 。
连接到测试数据库
测试DAO时,读者要有个疑问,测试数据库用到的数据源来自哪里,怎么让我们测试的DAO类来使用我们的数据源。执行测试实例的时候,Unitils 会根据我们定义的数据库连接属性来创建一个数据源实例连接到测试数据库。随后的DAO测试会重用相同的数据源实例。建立连接的细节定义在unitils.properties配置文件中,如代码清单16 27中的② 配置部分所示。
用Excel准备测试数据
准备好测试数据库之后,剩下的工作就是用Excel来准备测试数据及验证数据,回顾一下我们要测试的UserDaoImpl 类(代码清单16 24),需要对其中的获取用户信息方法findUserByUserName()及保存用户信息方法saveUser()进行测试,所以我们至少需要准备三个Excel数据集文件 ,分别是供查询用户用的数据集BaobaoTao.Users.xls、供保存用户信息用的数据集BaobaoTao.SaveUser.xls及供保存用户信息用的验证数据集BaobaoTao. ExpectedSaveUser.xls。下面以用户数据集BaobaoTao.Users.xls实例进行说明,如图16-8所示。

在①处t_user表示数据库对应的表名称。在②处表示数据库中t_user表对应的字段名称。在③处表示准备测试的模拟数据。一个数据集文件可以对应多张表,一个Sheet对就一张表。把创建好的数据集文件放到与测试类相同的目录中,如实例中的UserDaoTest类位于com.baobaotao.dao包中,则数据集文件需要放到当前包中。其他两个数据集文件数据结构如图16-9和16-10所示。

编写UserDaoImpl的测试用例
完成了Unitils环境配置、准备测试数据库及测试数据之后,就可以开始编写用户DAO单元测试类,下面我们为用户数据访问UserDaoImpl编写测试用例类。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- @SpringApplicationContext( {"baobaotao-dao.xml" }) //① 初始化Spring容器
- public class UserDaoTest extends UnitilsJUnit4 {
- @SpringBean("jdbcUserDao") //② 从Spring容器中加载DAO
- private UserDao userDao;
- @Before
- public void init() {
- }
- …
- }
在①处,通过Unitils提供@ SpringApplicationContext注解加载Spring配置文件,并初始化Spring容器。在②处,通过@SpringBean注解从Spring容器加载一个用户DAO实例。编写UserDaoTest测试基础模型之后,接下来就编写查询用户信息findUserByUserName()的测试方法。
代码清单16 31 UserDaoTest.findUserByUserName()测试
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- public class UserDaoTest extends UnitilsJUnit4 {
- …
- @Test //① 标志为测试方法
- @DataSet("BaobaoTao.Users.xls") //② 加载准备用户测试数据
- public void findUserByUserName() {
- User user = userDao.findUserByUserName("tony"); //③ 从数据库中加载tony用户
- assertNull("不存在用户名为tony的用户!", user);
- user = userDao.findUserByUserName("jan"); //④ 从数据库中加载jan用户
- assertNotNull("jan用户存在!", user);
- assertEquals("jan", user.getUserName());
- assertEquals("123456",user.getPassword());
- assertEquals(10,user.getCredits());
- }
- …
- }
在①处,通过JUnit提供@Test注解,把当前方法标志为可测试方法。在②处,通过Unitils提供的@DataSet注解从当前测试类UserDaoTest.class所在的目录寻找支持DbUnit的数据集文件并进行加载。执行测试逻辑之前,会把加载的数据集先持久化到测试数据库中,具体加载数据集的策略详见上文“配置数据集加载策略”部分。实例中采用的默认加载策略,即先删除测试数据库对应表的数据再插入数据集中的测试数据。这种策略可以避免不同测试方法加载数据集相互干扰。在③处执行查询用户方法时,测试数据库中t_user表数据已经是如图16-8 BaobaoTao.Users.xls所示的数据,因此查询不到“tony”用户信息。在④处,执行查询“jan”用户信息,从测试数据集可以看出,可以加载到“jan”的详细信息。最后在IDE中执行UserDaoTest. findUserByUserName()测试方法,按我们预期通过测试,测试结果如图16-11所示。

完成了查询用户的测试之后,我们开始着手编写保存用户信息的测试方法,详细的实现代码如下所示。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- …
- public class UserDaoTest extends UnitilsJUnit4 {
- …
- @Test //① 标志为测试方法
- @ExpectedDataSet("BaobaoTao.ExpectedSaveUser.xls") //准备验证数据
- public void saveUser()throws Exception {
- User u = new User();
- u.setUserId(1);
- u.setUserName("tom");
- u.setPassword("123456");
- u.setLastVisit(getDate("2011-06-06 08:00:00","yyyy-MM-dd HH:mm:ss"));
- u.setCredits(30);
- u.setLastIp("127.0.0.1");
- userDao.save(u); //执行用户信息更新操作
- }
- …
- }
在①处,通过JUnit提供@Test注解,把当前方法标志为可测试方法。在②处,通过Unitils提供的@ExpectedDataSet注解从当前测试类UserDaoTest.class所在的目录寻找支持DbUnit的验证数据集文件并进行加载,之后验证数据集里的数据和数据库中的数据是否一致。在UserDaoTest.saveUser()测试方法中创建一个User实例,并设置与图16-10 验证数据集中相同的数据,然后执行保存用户操作。最后在IDE中执行UserDaoTest.saveUser()测试方法,执行结果如图16-12所示。

虽然已经成功完成了保存用户信息UserDaoTest.saveUser() 方法测试,但还是存在不足的地方,我们测试数据通过硬编码方式直接设置在User实例中。如果需要更改测试数据,只能更改测试代码。大大削减了测试的灵活性。如果能直接从Excel数据集获取测试数据,并自动绑定到目标对象,那我们的测试用例就更加完美。为此笔者编写了一个获取Excel数据集Bean工厂XlsDataSetBeanFactory,用于自动绑定数据集到测试对象。我们对上面的测试方法进行整改,实现代码如代码清单16-33所示。
- import org.unitils.core.UnitilsException;
- import org.unitils.DbUnit.datasetfactory.DataSetFactory;
- import org.unitils.DbUnit.util.MultiSchemaDataSet;
- import sample.unitils.dataset.util.XlsDataSetBeanFactory;
- …
- public class UserDaoTest extends UnitilsJUnit4 {
- …
- @Test //① 标志为测试方法
- @ExpectedDataSet("BaobaoTao.ExpectedSaveUser.xls") //准备验证数据
- public void saveUser()throws Exception {
- //② 从保存数据集中创建Bean
- User u = XlsDataSetBeanFactory.createBean("BaobaoTao.SaveUser.xls”
- ,"t_user", User.class);
- userDao.save(u); //③ 执行用户信息更新操作
- }
- …
- }
在②处,通过XlsDataSetBeanFactory.createBean()方法,从当前测试类所在目录加载BaobaoTao.SaveUser.xls数据集文件,其数据结构如图16-9所示。把BaobaoTao.SaveUser.xls中名称为t_user 的Sheet页中的数据绑定到User对象,如果当前Sheet页有多条记录,可以通过XlsDataSetBeanFactory.createBeans()获取用户列表List<User>。最后在IDE中重新执行UserDaoTest.saveUser()测试方法,执行结果如图16-13所示。

从测试结果可以看出,执行UserDaoTest.saveUser()测试失败。从右边的失败报告信息我们可以看出,是由于模拟用户的积分与我们期望数据不一致造成,期望用户积分是30,而我们保存用户的积分是10。重新对比一下图16-9 BaobaoTao.SaveUser.xls数据集数据与图16-10 BaobaoTao.ExpectedSaveUser.xls数据集的数据,确实我们准备保存数据集的数据与验证结果的数据不一致。把BaobaoTao.SaveUser.xls数据集中的用户积分更改为30,最后在IDE中重新执行UserDaoTest.saveUser()测试方法,执行结果如图16-14所示。

从测试结果可以看出,保存用户通过测试。从上述的测试实战,我们已经体验到用Excel准备测试数据与验证数据带来的便捷性。到此,我们完成了DAO测试的整个过程,对于XlsDataSetBeanFactory具体实现,读者可以查看本章的实例源码,这里就不做详细分析。下面是实现基本骨架。
- import org.dbunit.dataset.Column;
- import org.dbunit.dataset.DataSetException;
- import org.dbunit.dataset.IDataSet;
- import org.dbunit.dataset.ITable;
- import org.dbunit.dataset.excel.XlsDataSet;
- …
- public class XlsDataSetBeanFactory {
- //从Excel数据集文件创建多个Bean
- public static <T> List<T> createBeans(String file, String tableName,
- Class<T> clazz) throws Exception {
- BeanUtilsBean beanUtils = createBeanUtils();
- List<Map<String, Object>> propsList = createProps(file, tableName);
- List<T> beans = new ArrayList<T>();
- for (Map<String, Object> props : propsList) {
- T bean = clazz.newInstance();
- beanUtils.populate(bean, props);
- beans.add(bean);
- }
- return beans;
- }
- //从Excel数据集文件创建多个Bean
- public static <T> T createBean(String file, String tableName, Class<T> clazz)
- throws Exception {
- BeanUtilsBean beanUtils = createBeanUtils();
- List<Map<String, Object>> propsList = createProps(file, tableName);
- T bean = clazz.newInstance();
- beanUtils.populate(bean, propsList.get(0));
- return bean;
- }
- …
- }
单元测试系列之5:使用unitils测试Service层
引述:Spring 的测试框架为我们提供一个强大的测试环境,解决日常单元测试中遇到的大部分测试难题:如运行多个测试用例和测试方法时,Spring上下文只需创建一次;数据库现场不受破坏;方便手工指定Spring配置文件、手工设定Spring容器是否需要重新加载等。但也存在不足的地方,基本上所有的Java应用都涉及数据库,带数据库应用系统的测试难点在于数据库测试数据的准备、维护、验证及清理。Spring 测试框架并不能很好地解决所有问题。要解决这些问题,必须整合多方资源,如DbUnit、Unitils、Mokito等。其中Unitils正是这样的一个测试框架。使用unitils测试Service层
在进行服务层的测试之前,我们先来认识一下需要测试的UserServiceImpl服务类。UserServiceImpl服务类中拥有一个处理用户登录的服务方法,其代码如下所示。
UserService.java
- package com.baobaotao.service;
- import com.baobaotao.domain.LoginLog;
- import com.baobaotao.domain.User;
- import com.baobaotao.dao.UserDao;
- import com.baobaotao.dao.LoginLogDao;
- @Service("userService")
- public class UserServiceImpl implements UserService {
- @Autowired
- private UserDao userDao;
- @Autowired
- private LoginLogDao loginLogDao;
- public void loginSuccess(User user) {
- user.setCredits( 5 + user.getCredits());
- LoginLog loginLog = new LoginLog();
- loginLog.setUserId(user.getUserId());
- loginLog.setIp(user.getLastIp());
- loginLog.setLoginTime(user.getLastVisit());
- userDao.updateLoginInfo(user);
- loginLogDao.insertLoginLog(loginLog);
- }
- …
- }
UserServiceImpl需要调用DAO层的UserDao和LoginLogDao以及User和LoginLog这两个PO完成业务逻辑,User和LoginLog分别对应t_user和t_login_log这两张数据库表。
在用户登录成功后调用UserServiceImpl中的loginSuccess()方法执行用户登录成功后的业务逻辑。
[1] 登录用户添加5个积分(t_user.credits)。
[2] 将登录用户的最后访问时间(t_user.last_visit)和IP(t_user.last_ip)更新为当前值。
[3] 在日志表(t_login_log)中为用户添加一条登录日志。
这是一个需要访问数据库并存在数据更改操作的业务方法,它工作在事务环境下。下面是装配该服务类Bean的Spring配置文件。
baobaotao-service.xml
- <?xml version="1.0" encoding="UTF-8" ?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:tx="http://www.springframework.org/schema/tx"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:p="http://www.springframework.org/schema/p"
- xmlns:aop="http://www.springframework.org/schema/aop"
- 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
- http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
- http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
- <context:component-scan base-package="com.baobaotao.service"/>
- <bean id="transactionManager"
- class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
- p:dataSource-ref="dataSource" />
- <tx:annotation-driven />
- <!-- 使用aop/tx命名空间配置事务管理,这里对service包下的服务类方法提供事务-->
- <aop:config>
- <aop:pointcut id="jdbcServiceMethod"
- expression= "within(com.baobaotao.service..*)" />
- <aop:advisor pointcut-ref="jdbcServiceMethod" advice-ref="jdbcTxAdvice" />
- </aop:config>
- <tx:advice id="jdbcTxAdvice" transaction-manager="transactionManager">
- <tx:attributes>
- <tx:method name="*"/>
- </tx:attributes>
- </tx:advice>
- </beans>
UserServiceImpl所关联的DAO类和PO类都比较简单,这里就不一一列出,读者可以参考本文附带光盘中的实例代码。在着手测试UserServiceImpl之前,需要先创建数据库表,相应的SQL脚本文件位于:D:\masterSpring\Chapter 16\schema目录下。
下面我们为UserServiceImpl编写一个简单的测试用例类,此时的目标是让这个基于Unitils测试框架的测试类运行起来,并联合Mockito框架创建Dao模拟对象。首先编写测试UserService#findUserByUserName()方法的测试用例,如代码清单16-37所示:
UserServiceTest.java
- package com.baobaotao.service;
- import org.unitils.UnitilsJUnit4;
- import org.unitils.spring.annotation.SpringApplicationContext;
- import org.springframework.test.util.ReflectionTestUtils;
- import org.unitils.spring.annotation.SpringBean;
- import org.junit.Test;
- import com.baobaotao.domain.User;
- import java.util.Date;
- …
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"}) //①加载Spring配置文件
- public class UserServiceTest extends UnitilsJUnit4{
- private UserDao userDao; //② 声明用户Dao
- private LoginLogDao loginLogDao;
- @Before //③ 创建Dao模拟对象
- public void init(){
- userDao = mock(UserDao.class);
- loginLogDao = mock(LoginLogDao.class);
- }
- @Test //④ 设置成为JUnit测试方法
- public void findUserByUserName() {
- //④-1 模拟测试数据
- User user = new User();
- user.setUserName("tom");
- user.setPassword("1234");
- user.setCredits(100);
- doReturn(user).when(userDao).findUserByUserName("tom");
- //④-2 实例化用户服务实例类
- UserServiceImpl userService = new UserServiceImpl();
- //④-3通过Spring测试框架提供的工具类为目标对象私有属性设值
- ReflectionTestUtils.setField(userService, "userDao", userDao);
- //④-4 验证服务方法
- User u = userService.findUserByUserName("tom");
- assertNotNull(u);
- assertThat(u.getUserName(),equalTo(user.getUserName()));
- //④-5 验证交互行为
- verify(userDao,times(1)).findUserByUserName("tom");
- }
- }
这里,我们让UserServiceTest直接继承于Unitils所提供的UnitilsJUnit4的抽象测试类,该抽象测试类的作用是让Unitils测试框架可以在JUnit 测试框架基础上运行起来。在①处,标注了一个类级的@SpringApplicationContext注解,这里Unitils将从类路径中加载Spring配置文件,并使用该配置文件启动Spring容器。在③处通过Mockito创建两个模拟DAO实例。在④-1处模拟测试数据并通过Mockito录制UserDao#findUserByUserName()行为。在④-2处实例化用户服务实例类,并在④-3处通过Spring测试框架提供的工具类org.springframework.test.util.ReflectionTestUtils为userService私有属性userDao赋值(ReflectionTestUtils是一个访问测试对象中私有属性非常好用的工具类)。在④-4处调用服务UserService#findUserByUserName()方法,并验证返回结果。在④-5处通过Mockito验证模拟userDao对象是否被调用,且只调用一次。最后在IDE中执行UserServiceTest测试用例,测试结果如图16-15所示。
从运行结果可以看出,我们已成功对UserServceTest.findUserByUserName()执行单元测试。下面我们通过Unitils提供的@DataSet注解来准备测试数据,并测试UserService# loginSuccess ()方法。BaobaoTao.SaveUsers.xls数据集如图16-16所示。

准备好了测试数据集之后,就可以开始为UserServiceImpl编写测试用例类,此时的目标是通过Unitils提供的@DataSet注解准备测试数据,来保证测试数据的独立性,避免手工通过事务回滚维护测试数据的状态。测试UserService#loginSuccess ()方法的代码如下所示。
代码清单16 38 UserServiceTest.java
- package com.baobaotao.service;
- import org.unitils.UnitilsJUnit4;
- import org.unitils.spring.annotation.SpringApplicationContext;
- import org.unitils.spring.annotation.SpringBean;
- import org.junit.Test;
- import com.baobaotao.domain.User;
- import java.util.Date;
- …
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"}) //①加载Spring配置文件
- public class UserServiceTest extends UnitilsJUnit4{
- //② 从Spring容器中加载UserService实例
- @SpringBean("userService")
- private UserService userService;
- @Test
- @DataSet("BaobaoTao.SaveUsers.xls")//③ 准备验证数据
- public void loginSuccess() {
- User user = userService.findUserByUserName("tom"); //④-1 加载"tom"用户信息
- Date now = new Date();
- user.setLastVisit(now); //④-2 设置当前登录时间
- userService.loginSuccess(user); //④-3 user登录成功,更新其积分及添加日志
- User u = userService.findUserByUserName("tom");
- assertThat(u.getCredits(),is(105)); //⑤ 验证登录成功之后,用户积分
- }
- }
在①处通过加载Unitils的@SpringApplicationContext 注解加载Spring配置文件,并初始化Spring容器。在②处通过@ SpringBean注解从Spring容器中获取UserService实例。在③处通过@DataSet注解从当前测试用例所在类路径中加载BaobaoTao.SaveUsers.xls数据集,并将数据集中的数据保存到测试数据库相应的表中。从上面的数据集中可以看出,我们为t_user表准备了两条用户信息测试数据。在④-1处从测试数据库中获取“tom”用户信息,模拟当前登录的用户。在④-2处设置当前“tom”用户的登录时间。在④-3处调用UserService#loginSuccess()方法,更新“tom”用户积分,并持久化到测试数据库中。在⑤处,验证“tom”用户当前积分是否是105分。完成测试用例的编写,最后在IDE中执行UserServiceTest测试用例,测试结果如图16-17所示。

从运行结果可以看出,我们已成功对UserServce#loginSuccess()执行单元测试。重复执行当前单元测试,测试结果仍然通过。细心的读者可能会有疑问,没有UserServce# loginSuccess()测试方法实施事务回滚,执行多次之后“tom”用户的积分不应该是105分,那为何测试还是通过呢?这是因为Unitils帮我们维护测试数据库中的数据状态,Unitils这个强大的魔力,归根于Unitils强大的数据集更新策略。到此我们成功完成UserServce单元测试。从上面为用户服务UserServce编写两个测试方法可以看出,对service层的测试,我们既可以采用JUnit+Unitils+Mockito组合,运用Mockito强大的模块能力,完成service层独立性测试,也可以采用JUnit+Unitils+Dbunit组合,运用Dbunit强大的数据库维护能力,完成service层+DAO层集成测试。