JUnit4 与 JMock 之双剑合璧

王建军,实习生,IBM

简介:在developerWorks上面有一些关于JUnit4的文章,也有关于JMock的文章,但是结合这两项技术的文章基本上没有。本文就基于这两种技术,对单元测试做一个全面的阐述。本文的目的不光是介绍层面的文字,更侧重作者在实践过程中的一些实际经验来展开分析。并且会针对测试用例的设计及单元测试常会遇到的一些困难等问题做一个全面的探讨。

引言

单元测试可以保证代码的质量,最大程度降低修复系统bug的时间和成本。能被称为测试的阶段有:单元测试、集成测试、系统测试和用户测试。修复系统bug的时间和成本随着这些阶段的推移呈指数级增长,特别是客户发现问题的时候,不仅是时间跟成本的损失,更是客户忠诚度的损失。由此可以看出单元测试的重要性。

JUnit是作为Java开发人员单元测试的利器。据统计,目前单元测试的框架中使用最多的是JUnit,占35%以上的比重。下面要介绍的是JUnit4,是JUnit最新的版本。JUnit4使用了Java5注解让测试的过程变得更方便和灵活,因此备受开发者的青睐。

通常待测的类不可避免地使用其他类的方法。在不能保证其他类方法正确性的前提下,如何通过单元测试的方式保证待测试的类方法是正确的呢?或者假如待测试的方法依赖的其他类的代码还没有实现而只是定义了接口,那么待测试的方法可以测试呢?JMock的出现解决了上面的问题。JMock提供给开发者切断待测方法对其他类依赖的能力,使开发者能够将全部的注意力都集中于待测方法的逻辑上,而不用担心其他类方法是否能够返回正确的结果。这样的测试更具有针对性,更容易定位到潜在问题。

因此,JUnit4JMock的组合就成为Java开发人员写单元测试必备的利器。本文对JUnit4JMock的组合使用、测试用例设计、常遇到的问题及可重用性方面进行了深入的探讨。

回页首

JUnit4应用

Eclipse是Java开发人员非常熟悉的IDE(集成开发环境)。目前我们选择Eclipse3.5以上版本,安装配置SunJDK1.5以上版本。Eclipse中已包含JUnit4Library,用户可以方便的导入使用。

由于在Developerworks上已经有许多介绍JUnit4应用的教程和文章,因此这里就不再赘述。对JUnit4还不熟悉的读者,本文推荐《深入探索JUnit4作为入门教程。

回页首

JMock应用于JUnit4

使用JMock,项目需要导入JMock相关的一系列Jar包。我们可以通过JMock的官方网站得到这些Jar包。这样项目中就可以使用JMock的技术来辅助我们更好的完成单元测试了。


清单1.依赖其他类方法的类TestJunit4.java

publicclassTestJunit4{

privateIMathfunutil;

publicTestJunit4(IMathfunutil){

this.util=util;

}

publicintcal(intnum){

return10*util.abs(num);

}

}

我们定义了一个很简单的类,在这个类中有cal方法,这个方法需要调用IMathfun接口中的abs方法。已知cal的作用就是对其参数取绝对值,然后再乘10返回。那么,cal方法依赖于接口IMathfun的方法abs完成取绝对值,我们可以通过MockIMathfun实例及方法来切断对其依赖性。


清单2.测试类TestJunit4Test.java

1publicclassTestJunit4TestextendsTestCase{

2privateMockerycontext=newJUnit4Mockery();

3privateIMathfunmath=null;

4privateTestJunit4test=null;

5@Before

6publicvoidsetUp()throwsException{

7super.setUp();

8math=context.mock(IMathfun.class);

9test=newTestJunit4(math);

10context.checking(newExpectations(){

11{

12exactly(1).of(math).abs(-10);will(returnValue(10));

13}

14});

15}

16@After

17publicvoidtearDown()throwsException{

18}

19@Test

20publicvoidtest(){

21assertEquals(100,test.cal(-10));

22}

23}

上面的代码中有几个需要注意地方:1.测试类继承了TestCaseline1)。2.创建了Mockery的对象contextline2)。3利用context对象来mock接口的实例(line8)及对应的方法(line12)。在这个简单的例子中我们mockmath及其方法,这样程序在运行的时候就不会去执行真正IMathfun实现类的代码,而转到line12的地方。给定数值-10,直接返回10

更多关于JMock的知识请参阅http://www.jmock.org/cookbook.html.

回页首

用例设计与Coverage分析

初学者在组合使用JUnit4JMock技术写单元测试的时候,往往只能发现很少的问题甚至发现不了问题。因此,开发人员就需要一些方法和指标来保证与规范单元测试的质量。其中单元测试用例设计原则和覆盖率指标就是重要的指导原则与指标。

GrenfordJ.Mayer在《TheArtofSoftwareTesting》一书中提出:一个好的测试用例(testcase)是指很可能找到迄今为止尚未发现的错误的测试。开发者不能只凭借自己主观或直观的想法来设计测试用例,应该要以一些比较成熟的测试用例设计方法为指导,再加上设计人员自己的经验积累才能做好用例设计。因此,也只有将用例设计方法与丰富的实践经验相融合才能设计出高质量的测试用例。

测试用例设计有三条基本指导原则:

1测试用例的代表性:能够代表并覆盖各种合理的和不合理、合法的和非法的、边界的和越界的以及极限的输入数据。

2测试结果的可判定性:测试执行结果的正确性是可判定的,每一个测试用例都应有相应的期望结果。

3测试结果的可再现性:即对同样的测试用例,系统的执行结果应当是相同的。

写测试用例的过程也是对代码进行重新审查的过程,首先需要弄清楚待测的方法的功能是什么。其次多角度考虑这个方法该怎么写才是正确的、可靠的、高效的。再次,多考虑一些边界跟异常的情况。

通常case的数量是由参数的个数决定的。对于每一个参数,我们需要设计三种情况对其方法进行测试,即:为null值,正确值和错误值。这样如果方法有n个参数,那么理论上case的数量需要3n个。这个数量所带来的工作量是非常大的,而且也是低效的。实际工作中,我们还需要另一个指标来衡量case的设计是否全面,那就是覆盖率。

Emma是一个Eclipse的标准插件,通过这个工具我们可以得到类级别的语句覆盖率(statementcoverage)和包级别的覆盖率(packagecoverage)。运行Emma后,工具会根据是否被执行将源代码行标注为不同的颜色,标记为红色的代码表示没有被执行,标记为绿色的代码表示已经被执行。我们可以通过以下步骤安装:

4在EclipseIDE中点击help->installnewsoftware.

5在弹出的页面地址栏中输入:http://update.eclemma.org/,会出现名为EclEmmaJavaCodeCoverage的插件,选中并安装。安装成功后工具栏会出现一个运行coverage的小图标。

覆盖率是另一个用来衡量测试用例是否全面的指标。关于coverage还可以细分为三个更小的指标:语句覆盖率(statementcoverage)、分支覆盖率(branchcoverage)、以及路径覆盖率(pathcoverage)。


1.三种覆盖率示例图

语句覆盖率是指方法中代码行被执行的百分比。例如上图中共有5条语句,如果执行的步骤是1-3-5。那么语句覆盖率是60%。如果要提高到100%的话,至少需要2case来覆盖:1-3-51-2-4-5。环境准备小节中提到的Emma可以提供语句覆盖率。具体执行过程:右键点击测试类->Coverageas->Junittest。语句的覆盖率会在coverage的视图中出现,并且根据是否被执行将源代码标注成不同的颜色。

分支覆盖率指的是方法中分支被执行的百分比。例如图1中在第一个菱形的位置处分成二个分支,在第二个菱形的位置处分成二个分支。这样共有4个分支。如果执行的步骤是1-2-3-5的话,因为只执行了其中的两个分支,所以分支覆盖率是50%。如果要提高到100%的话,至少需要2case来覆盖:1-2-3-51-F-4-5F第一个分支处false)。分支的覆盖率要比语句的覆盖率更能够保证case的质量。目前Emma还不能生成分支覆盖率。Cobertura是一个可以生成语句覆盖率跟分支覆盖率的强大工具。Cobertura的使用比Emma复杂一些,需要写ant脚本来运行Cobertura。利用Cobertura生成html,xml等文件来显示类的语句及分支覆盖率的情况。

路径覆盖率指的是方法中路径被执行的百分比。这是一个比上面两个指标更全面的指标。路径指的是从方法的起点到终点的一条通路,如:1-2-3-5。如果想要路径的覆盖率达到100%,那么必须要4case才能完成:1-2-3-5,1-2-4-51-F-3-51-F-4-5。目前也有一些工具可以用来生成路径覆盖率。

在测试用例设计的过程中,我们需要兼顾用例设计的指导原则与覆盖率的一些指标,再结果开发人员自身的实践经验必定能设计出好的测试用例。

回页首

可能遇到的困难

组合使用JUnit4JMock写单元测试时,通常情况下类的覆盖率难以达到100%,源于某些方法不能直接写测试。在这一小节里,对常见的问题提供了一些解决途径。

静态方法

静态方法是单元测试过程中常见的困难之一。程序中通常需要调用其他的类的静态方法获取运行的数据或者参数。在这种情况下,只有程序启动以后这些参数才是存在的,而单元测试却是在程序未启动的情况下执行的。静态方法是通过类调用,而不是对象调用,因此也不能通过JMock的方式解决。


清单3.TestStatic.java

publicclassTestStatic{

publicTestStatic(){}

publicintgetOSType(){

Stringos=Utils.getTargetOS();

if("Windows".equals(os))

Return0;

elseif("Linux".equals(os)||"Unix".equals(os))

Return1;

else

Return2;

}

}

TestStatic类的getOSType方法写单元测试的话,由于需要调用Utils类的静态方法getTargetOS。所以,如果不做任何处理的话,单元测试是不能进行的。有两种方法可以解决这个问题。

6检查Utils类是否有带public修饰符类似于setTargetOS的方法。如果有并且能够通过调用的这个方法达到设置OS目的的话,我们只需要在测试方法调用此方法设置即可完成。

7如果上述情况不能完成。那么考虑第二种办法-重构TestStaticUtils。但是,这么做会引发一些争议。为了测试而修改源代码,值得吗?这是需要开发者去权衡的问题。。对于Utils的改动很小,只需要将其方法getTargetOS的修饰符static去掉。类TestStatic的修改变化要大一些:


清单4.修改后的TestStatic.java

publicclassTestStatic{

privateIUtilsutils=null;

publicTestStatic(IUtilsutils){

This.utils=utils;

}

publicintgetOSType(){

Stringos=utils.getTargetOS();

if("Windows".equals(os))

Return0;

elseif("Linux".equals(os)||"Unix".equals(os))

Return1;

else

Return2;

}

}

TestStatic.java的修该有几个地方:1.增加了utils的成员变量。2.修改了构造函数。3.将类中调用外部类的静态方法改为调用非静态方法。


清单5.TestStaticTest.java

publicclassTestStaticTestextendsTestCase{

Mockerycontext=newJUnit4Mockery();

IUtilsutils=null;

TestStaticstat=null;

@Before

publicvoidsetUp(){

utils=context.mock(IUtils.class);

Stat=newTestStatic(utils);

}

@After

publicvoidtearDown(){}

@Test

publicTestOSTypeWin(){

context.checking(newExpectations(){

{

exactly(1).of(utils).getTargetOS();will(returnValue("Windows"));

}

});

assertEquals(0,stat.getOSType());

}

@Test

publicTestOSTypeLN(){

context.checking(newExpectations(){

{

atLeast(1).of(utils).getTargetOS();

/*27*/will(onConsecutiveCalls(

returnValue("Linux"),

returnValue("Unix")));

}

});

assertEquals(1,stat.getOSType());//returnvalue:Linux

assertEquals(1,stat.getOSType());//returnvalue:Unix

}

}

这里需要解释一下第27行,这样写的结果是:如果函数第一次被调用则返回"Linux",如果是第二次调用返回"Unix"

私有方法

私有方法也是写单元测试过程中常会遇到的困难。类中的私有方法主要是供公有方法调用。测试公有方法之前需要保证私有方法的正确性。通常,开发者会在测公有方法之时测私有方法。这么做会产生一些问题,首先,为了测公有方法里的某一个私有方法,我们需要重复此私有方法调用之前的工作。其次,如果一个公有方法里调用大量私有方法时,用这种方法的写出来的测试代码会非常复杂,不利于测试。如果我们能够首先独立测试私有方法,那么就会极大地减轻公有方法的测试工作量。

解决这个问题,有两种方法。

8由于开发者通常会将测试类跟待测试的类置于同一包下,所以,最简单的方法是将私有方法前的修饰符修改为protected,这样的话测试代码就可以访问原来的私有方法了。值得注意的是这样做需要修改源代码。

9另一种方法不需要修改源代码,利用Java反射可以实现。


清单6.Data.java

publicclassData{

privateStringname="Na";

publicStringgetName(){

returnname;

}

privatevoidsetName(Stringstr){

name=str;

}

}

Data类中的setName是私有方法。我们可以通过反射的方式对其测试。


清单7.DataTest.java

publicclassDataTestextendsTestCase{

@Before

publicvoidsetUp(){

}

@After

publicvoidtearDown(){

}

@Test

publicvoidtest()throwsException{

Datadata=newData();

System.out.println(data.getName());

Methodm=data.getClass().getDeclaredMethod("setName",String.class);

m.setAccessible(true);

m.invoke(data,"Joh");

System.out.println(data.getName());

}

}



清单8.输出结果

Na

Joh

从输出结果我们可以看出,我们通过反射调用了Data的私有方法setName,成功将其name值改变为Joh。关于Java反射的特性,可以参阅Java编程思想。

JMock的一些问题

10MockconcreteObject

由于目前推崇面向接口(interface)的设计,Mockery对象(context)的mock方法通常的参数是接口类型(context.mockInterface.class))。但是,实际情况中我们偶然会遇到mock实体对象的情况。如果需要mock实体对象的话,跟mock接口有所不同,不能通过直接传实体类来构造。在mock实体对象之前,需要调用context的一个方法:setImposteriser(ClassImposteriser.INSTANCE),同时需要导入相关的三个jar包。

11Mock对象的方法多次调用返回不同结果

在代码清单8中已经给出了一个例子,对象utils第一次调用getTargetOS返回"Linux",第二次调用返回"Unix"

12Mock对象的方法参数不能指定具体值

有些时候我们在mock对象的方法时,不能获取相应的参数时,解决办法是指定一个类型即可。例如:exactly(1).of(computer).getOSType(with(any(String.class)));will(returnValue(2));

如果有多个参数时,参数的匹配模式必须是一致的。也就是说要么参数都是指定类型的,要么参数都是具体值的,混合使用会由于匹配模式不一致而抛出异常。另外,数组的类型比较特别,如String数组的类型为String[].class

13方法没有返回值,但是会改变其成员的值。

遇到这种情况会比较复杂,需要定义一个类去实现Action接口。详细见http://www.jmock.org/custom-actions.html

回页首

可重用性方面

组合使用JUnit4JMock写单元测试如果没有使用重用性技巧会产生大量重复性的劳动。可重用性分为两个方面:多个类的可重用性和单类的可重用性。

常常会遇到一种情况:在多个测试类中都需要mock相同对象及其方法,我们可以将这一过程提取出来。通常测试类都继承于TestCase,为了实现重用性,需要再定义一个类,取名为GeneralTest,使GeneralTest继承TestCase。在GeneralTest.java中将在多个测试类中用到的相同对象的mock工作封装进去。然后其他的测试类全部继承自GeneralTest,这样测试类中就可以反复使用GeneralTest提供给我们的方法来mock对象了,我们需要做的就是简单传入几个参数而已。这样不仅能够省去大量的重复劳动,还能使测试代码看起来更简单、清晰,给我们的测试工作带来非常大的好处。

在单个测试类中也可以提高可重用性。在单个测试类中也会有一些需要mock的对象在多个测试方法重用。可以将这些可重用的mock对象作为测试类的成员,将这些对象的mock工作置于@Before修饰的方法中,那么在需要用到这些对象的时候直接使用即可,不需要在每一个测试方法中重新做一遍。所带来的好处与上面一种情况是类似的。

回页首

结束语

对于Java而言,大部分开发者会使用到JUnit4JMock这两项技术。本文循序渐进地对JUnit4应用、JMockJUnit4的结合使用、用例设计、困难解决和可重用性多个阶段全面地阐述了作者在单元测试过程中的心得。在介绍的过程中,同时也会照顾一些入门级的读者,对一些操作给出了详细的步骤,并提供了大量的示例。

希望入门级的读者在阅读这篇文章后对单元测试有一个全面的认识,更希望有经验的读者阅读后会有所收获。

参考资料

学习

·www.jmock.org/cookbook.htmJMock官方网站

·http://www.junit.orgJUnit官方网站

·“让编译和测试过程自动化AntJUnit让您与XP梦想更近一步”(developerWorks200110月):逐步递增测试和持续编译是极端编程方法基础的两种。把两者合并成为一个单独的、自动进行的过程加上自动生成电子邮件报告您就将在向XP梦想前进的道路上迈出坚实的步子。请跟随ErikHatcher,他向您展示了他是如何修改流行的Ant1.3JUnit测试框架,以达到让编译和测试过程完全的、用户化的自动化。

·“Jython构建JUnit测试包”(developerWorks20045月):开发人员有多种理由决定自动化单元测试。许多人甚至进一步发挥它,自动化这些测试的定位和执行。但是如果想要测试装具模块(testharness)像静态定义的那样运行呢?请跟随开发员MichaelNadel,看看如何利用Python模拟静态定义的JUnitTestSuite类。

·“JUnit4抢先看”(developerWorks200510月):JUnitJava语言事实上的标准单元测试库。JUnit4是该库三年以来最具里程碑意义的一次发布。它的新特性主要是通过采用Java5中的标记(annotation)而不是利用子类、反射或命名机制来识别测试,从而简化测试。在本文中,执着的代码测试人员ElliotteHaroldJUnit4为例,详细介绍了如何在自己的工作中使用这个新框架。注意,本文假设读者具有JUnit的使用经验。

·developerWorksJava技术专区:这里有数百篇关于Java编程各个方面的文章。

讨论

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

关于作者

王建军,浙江大学软件工程硕士。主要研究方向是JavaapplicationJ2EE应用开发。曾经在上海IBMCDL实习5个月,目前在上海IBMCSTL实习,在单元测试方面有较长时间的实践。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值