JMockit工具总结
JMockit is a Java toolkit for automated developer testing.It contains APIs for the creation of the objects to be tested, for mocking dependencies, and for faking externalAPIs; JUnit (4 and 5) and TestNG test runners are supported.It also contains an advanced code coverage tool.
Jmockit是一个Java工具,用于开发者做自动测试。Jmockit包含了被创建对象的创建API,主要是为了模拟外部依赖和外部API,支持Junit4、5和TestNG,并且Jmockit还包含了优秀的代码覆盖工具。
JMockit简介
JMockit是一个用于在测试阶段模拟Java对象的Java框架,支持Junit4和TestNG。JMockit使用Java的instrumentation API在运行时修改类class文件的字节码来动态的改变类的行为。JMockit比较强的地方在于它的可表达性和开箱即用的能力,它支持模拟静态方法和私有方法(不推荐mock私有方法)。
Jmockit具有超强的表达能力。我们只需要创建mocks并定义其行为,我们不需要进行各种链式调用,只要直接定义它们就好。也就是说你不会有如下这样的操作。
而是直接定义mock的行为即可。
可能看起来代码变多了,但是你完全可以将它们写在一行。重点在于,我们不需要在写冗长的链式调用代码了。你只需要定义你想mock对象方法调用时有什么行为即可。另外result=value部分,你可以完全返回任何你想要的值(固定值、动态生成的值、异常等等),可以看到JMockit有极强的表达力。
JMockit的Record-Replay-Verify模型
使用JMockit进行测试分为三个不同的阶段:录制、重放和核实。
1. 录制阶段,我们为下一步所需要用到的测试都定义默认的行为。
2. 重放阶段,也就是真正执行测试代码的阶段,之前定义的模拟方法,构造器等等都会在这个阶段播放。
3. 最后,在核实阶段,我们将对测试的结果进行断言,来确认和我们的预期是一致的。
我们通过一个简单的例子来了解下JMockit的精髓吧。
在上面的例子中,通过参数Mock了一个对象,定义了两个Expectations, 一个Verification。整个流程读下来就是,我希望我Mock的对象的sum(1,2)返回3,sum(2,3)返回5,然后开始使用Mock的对象来执行代码;接下来确认Mock对象的sum(int,int)方法被调用了2次;最后使用两个断言来看程序执行结果是否符合自己的预期。
Mock对象的创建
当我们使用JMockit时,最简单的创建mocks的方法就是使用注解了。这里有3个注解来创建mocks, @Mocked、@Injectable、@Capturing。
当我们使用@Mocked注解来标注一个成员变量,它将会为每个该类的实例对象创建一个mocked实例,即使你new出来的实例也会受到影响。但是,使用@Injectable注解来标注一个成员变量,只会对标注的成员变量实例进行模拟,其他实例不受影响,你new出来的对象仍然保持原有行为。
我们通过一个简单的例子来理解下。
但是如果我们将@Mocked换成@Injectable, 我们得到的结果将会是下面这样。
@Capturing注解行为和@Mocked一样,但是会为被标注变量所属类的子类或实现类进行模拟,也就是上述TestCalculator的子类的sum(1,2)也会返回4。
@Tested注解常常和@Injectable注解一起使用,@Tested用来表示要测试的对象,@Injectable一般用来模拟主业务中其他的依赖的对象。
比如现在有一个业务,用户上传头像的业务,涉及FileService和UserService两个类的操作,但是我们现在无法搭建文件服务,所以我们决定模拟文件服务,我们通过代码来看一下。
上述代码中的主要测试点是UserService实例的saveLogoKey()方法,因此我们用@Tested来标注UserService,但是我们又缺少FileService的环境,所以我们用@Injectable来标注FileService,并使用Expectations来模拟其行为,使其返回一个fake file unique key, 那么userService就可以走通了。
Expectations & Verifications
接下来的方法将用到Expectations和Verifications。
any通配参数
Jmockit提供了一系列的通用字段:anyInt、anyString等等。以此来让参数的匹配更加通用,这些通用字段都是以any开头的。比如我们想给一个mock对象的f(String s)方法接受任何参数都返回true,我们就可以在Expectations中使用通用字段。
注:在使用Expectations时,里面定义的方法期望必须在Expectations下面的代码段有调用,要不然会报错Missing Invocation。
注:当使用Any工具字段的时候,我们必须要将参数强转到方法所接受的类型,如上面代码所示。
with通配方法
JMockit还提供了很多通用方法去处理通用参数匹配问题,这些方法都以With开头(withSubString(subString)、withNotEqual(1)),这些方法比any开头的字段更加高级,我们可以看下面这个例。
上面的例子中使用了withEqual(1)和withNotEqual(2),表示calculator在sum()的第一个参数为1时、第二个参数不为2时,返回结果是4。
null is NOT null
null是一个定义任意引用对象的语法糖,如果你想验证当前方法接受的参数是null的引用,我们可以使用withNull()。
下面的例子,我们将定义mock对象的行为,这个行为将会被触发,如果传入的参数是String、List和null的引用。
可以注意到区别:null意味着任何list,withNull()意味着null的list的引用。特别是,为了避免将值强转到声明的参数类型时。
这个的唯一使用场景就是,至少有一个详细的参数匹配器被用到Expections中(要么是Any要么是With)。
times限制模拟对象调用次数
有时候,我们需要限制mock方法的调用次数。JMockit使用times、minTimes和maxTimes来达成目的。我们通过一个小例子来体验一下。
上面的例子在Expectations使用了times=2来期望方法会被调用2次,如果不够2次或超过2次都将产生错误。
自定义通用类型
JMockit还允许自定义通用类型,使用withArgThat和BaseMatcher来实现。
Results和Returns
JMockit使用Result和Returns来模拟mock对象的返回值,Result和Returns可以覆盖90%的返回值类型,我们通过一个例子来了解下。
注: Returning只能用在Expectations中。
JMockit还支持第三种方式来返回值。
上述例子使用代理来返回值,如果传递的参数<3, 则返回5, 否则抛出异常。
JMockit高级方案
JMockit除了提供了上述的Expectations和Verifications等特性之外,还提供了其他的高级特性。
• Faking(MockUp API)
• Deencapsulation功能类
• 如何使用一个mock来模拟多个接口
• 如何重用Expectations和Verifications
在原先JMockit版本中,支持对私有变量对的mock, 但是在我测试的1.43版本中,不再支持对private字段和方法进行模拟,因为对于私有方法和内部类的测试往往是不被推荐的。
MockUpAPI
除了可以使用Expectations和Verifications的特性,我们还可以使用MockUp类来进行字段、方法的模拟(静态方法和静态变量不推荐使用MockUp),并且MockUp貌似只能模拟类,不能模拟接口。
可以看到,上述方法没有对max方法进行模拟,则会导致调用方法时出现actual=0的情况。
Deencapsulation
接下来,我们使用Deencapsulation来模拟公有字段。
如何在一个测试类中模拟多个接口
假设我们现在想测试一个类,但是还没有实现,但是我们很清楚的知道它将会实现多个接口。我们可以使用泛型和定义一个类来实现那多个接口。
我们将通过下面的例子来看看如何在一个测试类下模拟多个接口。
总结
JMockit为我们单元测试引入了模拟对象方案,有了JMockit我们基本上可以对任何类、接口、方法、字段进行模拟,来隔离外部依赖,从而写出清晰易读的测试代码。