(来自IBM) JUnit4 与 JMock 之双剑合璧
。
覆盖率是另一个用来衡量测试用例是否全面的指标。关于 coverage 还可以细分为三个更小的指标:语句覆盖率(statement coverage)、分支覆盖率(branch coverage)、以及路径覆盖率(path coverage)。
图 1.三种覆盖率示例图
语句覆盖率是指方法中代码行被执行的百分比。例如上图中共有 5 条语句,如果执行的步骤是 1-3-5。那么语句覆盖率是 60%。如果要提高到 100% 的话,至少需要 2 个 case 来覆盖:1-3-5 与 1-2-4-5。环境准备小节中提到的 Emma 可以提供语句覆盖率。具体执行过程:右键点击测试类 ->Coverage as->Junit test。语句的覆盖率会在 coverage 的视图中出现,并且根据是否被执行将源代码标注成不同的颜色。
分支覆盖率指的是方法中分支被执行的百分比。例如图 1 中在第一个菱形的位置处分成二个分支,在第二个菱形的位置处分成二个分支。这样共有 4 个分支。如果执行的步骤是 1-2-3-5 的话,因为只执行了其中的两个分支,所以分支覆盖率是 50%。如果要提高到 100% 的话,至少需要 2 个 case 来覆盖:1-2-3-5 与 1-F-4-5(F 第一个分支处 false)。分支的覆盖率要比语句的覆盖率更能够保证 case 的质量。目前 Emma 还不能生成分支覆盖率。Cobertura 是一个可以生成语句覆盖率跟分支覆盖率的强大工具。Cobertura 的使用比 Emma 复杂一些,需要写 ant 脚本来运行 Cobertura。利用 Cobertura 生成 html, xml 等文件来显示类的语句及分支覆盖率的情况。
路径覆盖率指的是方法中路径被执行的百分比。这是一个比上面两个指标更全面的指标。路径指的是从方法的起点到终点的一条通路,如:1-2-3-5。如果想要路径的覆盖率达到 100%,那么必须要 4 个 case 才能完成:1-2-3-5,1-2-4-5,1-F-3-5,1-F-4-5。目前也有一些工具可以用来生成路径覆盖率。
在测试用例设计的过程中,我们需要兼顾用例设计的指导原则与覆盖率的一些指标,再结果开发人员自身的实践经验必定能设计出好的测试用例。
回页首
可能遇到的困难
组合使用 JUnit4 与 JMock 写单元测试时,通常情况下类的覆盖率难以达到 100%,源于某些方法不能直接写测试。在这一小节里,对常见的问题提供了一些解决途径。
静态方法
静态方法是单元测试过程中常见的困难之一。程序中通常需要调用其他的类的静态方法获取运行的数据或者参数。在这种情况下,只有程序启动以后这些参数才是存在的,而单元测试却是在程序未启动的情况下执行的。静态方法是通过类调用,而不是对象调用,因此也不能通过 JMock 的方式解决。
清单 3. TestStatic.java
public class TestStatic{ public TestStatic(){} public int getOSType(){ String os = Utils.getTargetOS(); if("Windows".equals(os)) Return 0; else if("Linux".equals(os)||"Unix".equals(os)) Return 1; else Return 2; } }
?
对
TestStatic类的
getOSType方法写单元测试的话,由于需要调用
Utils类的静态方法
getTargetOS。所以,如果不做任何处理的话,单元测试是不能进行的。有两种方法可以解决这个问题。
检查
Utils类是否有带
public修饰符类似于
setTargetOS的方法。如果有并且能够通过调用的这个方法达到设置
OS目的的话,我们只需要在测试方法调用此方法设置即可完成。
如果上述情况不能完成。那么考虑第二种办法 - 重构
TestStatic与
Utils。但是,这么做会引发一些争议。为了测试而修改源代码,值得吗?这是需要开发者去权衡的问题。。对于
Utils的改动很小,只需要将其方法
getTargetOS的修饰符
static去掉。类
TestStatic的修改变化要大一些:
清单 4. 修改后的 TestStatic.java
public class TestStatic{ private IUtils utils = null; public TestStatic(IUtils utils){ This.utils = utils; } public int getOSType(){ String os = utils.getTargetOS(); if("Windows".equals(os)) Return 0; else if("Linux".equals(os)||"Unix".equals(os)) Return 1; else Return 2; } }
?
TestStatic.java的修该有几个地方:1. 增加了
utils的成员变量。2. 修改了构造函数。3. 将类中调用外部类的静态方法改为调用非静态方法。
清单 5. TestStaticTest.java
public class TestStaticTest extends TestCase{ Mockery context = new JUnit4Mockery(); IUtils utils = null; TestStatic stat = null; @Before public void setUp(){ utils = context.mock(IUtils.class); Stat = new TestStatic(utils); } @After public void tearDown(){} @Test public TestOSTypeWin(){ context.checking(new Expectations(){ { exactly(1).of(utils).getTargetOS();will(returnValue("Windows")); } }); assertEquals(0,stat.getOSType()); } @Test public TestOSTypeLN(){ context.checking(new Expectations(){ { atLeast(1).of(utils).getTargetOS(); /*27*/ will(onConsecutiveCalls( returnValue("Linux"), returnValue("Unix"))); } }); assertEquals(1,stat.getOSType());// return value:Linux assertEquals(1,stat.getOSType());// return value:Unix } }
?
这里需要解释一下第
27行,这样写的结果是:如果函数第一次被调用则返回
"Linux",如果是第二次调用返回
"Unix"。
私有方法
私有方法也是写单元测试过程中常会遇到的困难。类中的私有方法主要是供公有方法调用。测试公有方法之前需要保证私有方法的正确性。通常,开发者会在测公有方法之时测私有方法。这么做会产生一些问题,首先,为了测公有方法里的某一个私有方法,我们需要重复此私有方法调用之前的工作。其次,如果一个公有方法里调用大量私有方法时,用这种方法的写出来的测试代码会非常复杂,不利于测试。如果我们能够首先独立测试私有方法,那么就会极大地减轻公有方法的测试工作量。
解决这个问题,有两种方法。
清单 6.Data.java
public class Data { private String name = "Na"; public String getName(){ return name; } private void setName(String str){ name = str; } }
?
Data类中的
setName是私有方法。我们可以通过反射的方式对其测试。
清单 7. DataTest.java
public class DataTest extends TestCase{ @Before public void setUp(){ } @After public void tearDown(){ } @Test public void test() throws Exception{ Data data = new Data(); System.out.println(data.getName()); Method m = 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 的一些问题
由于目前推崇面向接口(interface)的设计,Mockery 对象(context)的 mock 方法通常的参数是接口类型(context.mock(Interface.class))。但是,实际情况中我们偶然会遇到 mock 实体对象的情况。如果需要 mock 实体对象的话,跟 mock 接口有所不同,不能通过直接传实体类来构造。在 mock 实体对象之前,需要调用 context 的一个方法:setImposteriser(ClassImposteriser.INSTANCE),同时需要导入相关的三个 jar 包。
在代码清单 8 中已经给出了一个例子,对象 utils 第一次调用 getTargetOS 返回"Linux",第二次调用返回"Unix"。
Mock 对象的方法参数不能指定具体值有些时候我们在 mock 对象的方法时,不能获取相应的参数时,解决办法是指定一个类型即可。例如:exactly(1).of(computer).getOSType(with(any(String.class)));will(returnValue(2));
如果有多个参数时,参数的匹配模式必须是一致的。也就是说要么参数都是指定类型的,要么参数都是具体值的,混合使用会由于匹配模式不一致而抛出异常。另外,数组的类型比较特别,如 String 数组的类型为 String[].class。
方法没有返回值,但是会改变其成员的值。遇到这种情况会比较复杂,需要定义一个类去实现 Action 接口。详细见 http://www.jmock.org/custom-actions.html 。
回页首
可重用性方面
组合使用 JUnit4 和 JMock 写单元测试如果没有使用重用性技巧会产生大量重复性的劳动。可重用性分为两个方面:多个类的可重用性和单类的可重用性。
常常会遇到一种情况:在多个测试类中都需要 mock 相同对象及其方法,我们可以将这一过程提取出来。通常测试类都继承于 TestCase,为了实现重用性,需要再定义一个类,取名为 GeneralTest,使 GeneralTest 继承 TestCase。在 GeneralTest.java 中将在多个测试类中用到的相同对象的 mock 工作封装进去。然后其他的测试类全部继承自 GeneralTest,这样测试类中就可以反复使用 GeneralTest 提供给我们的方法来 mock 对象了,我们需要做的就是简单传入几个参数而已。这样不仅能够省去大量的重复劳动,还能使测试代码看起来更简单、清晰,给我们的测试工作带来非常大的好处。
在单个测试类中也可以提高可重用性。在单个测试类中也会有一些需要 mock 的对象在多个测试方法重用。可以将这些可重用的 mock 对象作为测试类的成员,将这些对象的 mock 工作置于 @Before 修饰的方法中,那么在需要用到这些对象的时候直接使用即可,不需要在每一个测试方法中重新做一遍。所带来的好处与上面一种情况是类似的。
回页首
结束语
对于 Java 而言,大部分开发者会使用到 JUnit4 跟 JMock 这两项技术。本文循序渐进地对 JUnit4 应用、JMock 与 JUnit4 的结合使用、用例设计、困难解决和可重用性多个阶段全面地阐述了作者在单元测试过程中的心得。在介绍的过程中,同时也会照顾一些入门级的读者,对一些操作给出了详细的步骤,并提供了大量的示例。
希望入门级的读者在阅读这篇文章后对单元测试有一个全面的认识,更希望有经验的读者阅读后会有所收获。
<!-- CMA ID: 656308 --><!-- Site ID: 10 --><!-- XSLT stylesheet used to transform this file: dw-document-html-6.0.xsl -->?
参考资料
学习
www.jmock.org/cookbook.htm:JMock 官方网站讨论
加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。?
?
?
Link: http://www.ibm.com/developerworks/cn/java/j-lo-junit4jmock/index.html