简介
翻译自官方文档
Jmockit主要有两个部分组成:Mocking(也称“Expectation”)和Faking,Faking下一章会讲。实际上大部分测试中,Faking用的比Mocking要多。
Mocking有一套流程:Exceptations, replay,和Verfications,Exceptations也就是期望,它会记录期望行为,常常就是我们希望伪造的部分;replay是回放,是调用我们想测试的代码的时候;Verfications是验证(可以使用各种assert),它的语法和Exceptations类似,是用来验证我们的最后的结果符不符合期望的逻辑。
我的理解:
mocking关注输入输出,对于给定输入,moking能够产生期望的输出,它不倾向去更改mock对象的内部实现来实现期望输出
faking常常针对某个方法或某个类的内部实现进行伪造(内部搞破坏),通过改变部分代码的运行逻辑产生期望的结果
Jmockit能够mock public方法,final,static方法方法和构造函数。当mock一个对象后,在原对象运行的时候将会使用mock后的对象来代替运行。
一个案例
假设现在有一个sevice类,需要完成这些操作:
- 查询数据库
- 数据入库
- 发邮件通知
对于LLT(low level test)来说,我们需要验证程序的内部逻辑是否正确,在操作数据库、发邮件时只需要得到正确的反馈(访问数据库、发邮件都能操作一般都是依赖第三方,这些第三方的东西一般都是假定正确的),而不需关注(通常也没法配置,因为环境是变化的)要连接哪个数据库,给哪个地址发邮件等操作,因此这些跟我们程序的内部逻辑不是很相关或者需要依赖真实环境的部分可以mock掉。
这个案例发邮件是mock的,至于实体类和数据配置以及操作可以忽略。
定义实体类:
1 |
|
Database类:
1 | public final class Database |
存库和查询时使用到的内存数据库HSQL的配置(这里查数据库不是mock的,是通过查询实际的内存数据库得到数据,因此不用太关注):
1 | <?xml version="1.0" encoding="UTF-8"?> |
service类:
1 | public final class MyBusinessService |
主要的测试代码:
@Test注解负责正确的初始化相关对象,有点像Spring的@Component
@Mocked 注解标识相关的类将会被Mock掉
Verifications 表示验证阶段,例如,{ anyEmail.send(); times = 1; }表示上面代码运行过程中,anyEmail.send运行了一次
Expectations 表示期望,可以让我们mock的对象产生期望的运行结果。这能够帮助我们在测试在不同的条件(例如是否出现异常)下代码逻辑的正确性。
1 | public final class MyBusinessServiceTest |
Mocking
jmockit中Expectations Api 主要提供了mocking的功能,mocking主要关注代码的行为,用于测试比较独立的两个模块间的交互功能,也就是当一个unit(一个类或者模块)依赖另一个unit时,我们可以将另一个unit mock掉。
Mock的类型和实例
方法以及构造器是moking主要的对象。主要有两种moking对象的声明形式:作为一个类的属性、作为方法中的一个参数(传参)
mock的type(类型)可以是接口,类以及注解或者枚举。在测试阶段,mocked type的所有非私有方法和所有非私有构造器都会被mock,如果mock的类型是一个类,它的所有父类(不包括Object)也会被mock。
当一个方法或构造器被mock,在测试的时候,具体的实现将会有Jmockit掌管。
案例代码如下:
若mock的对象作为方法的参数,该对象将会由Jmockit来创建,然后传递给JUunit等test runner,所以参数不能为nulll
若mock的对象作为属性变量,Jmocki将会创建后赋值给这个变量,前提是它不能为final
1 | // "Dependency" is mocked for all tests in this test class. |
相关注解的差异:
@Mocked 主要的mocking注解,可以带一个属性stubOutClassInitialization
@Injectable 单例,只会mock一个实例的实例方法
@Capturing 将会mock所有实现该mock接口的类
期望(Expectations)
期望中一个方法可以出现多次,实际运行中的方法匹配expectation不仅是看方法签名,也看传递的参数的值是否一致。
1 |
|
record-replay-verify 模型
测试过程可以被分为三个阶段:
准备阶段:初始化相关的类
测试代码执行阶段
测试结果验证阶段
但是在基于(期望的)行为的测试中,我们可以将测试分为:
记录阶段: 所谓记录就是记录我们mock的对象的行为,实际上就是上面的期望(记录不如说是期望)
回放阶段: 也就是代码实际执行阶段(回放我们上面的记录)
验证阶段: 验证代码是否按照期望进行运行(各种assert)
下面是几种常见rrv模型出现的形式:
1 | public class SomeTest |
测试类的初始化和注入
@Tested 注解的实例属性将会被自动初始化和注入,如果在测试方法执行前仍为null,将会调用默认构造函数(可以保证不为null)
@Injectable 对象的注入,可以不是mock对象
@Mocked或者@Capturing不会进行对象注入
1 | public class SomeTest |
Record
记录阶段可以记录返回值(result=),在回放阶段,相应的方法被调用后就会返回记录的值
如果希望有异常出现可以用result赋值
result多次赋值表示可以返回多个结果(依次的,不是同时返回)
也可以用return(value1,value2)的形式来返回多个值
1 | public class ClassUnderTest |
1 | ClassUnderTest cut; |
灵活的参数匹配
注意:期望记录中的方法如果有具体参数,那么在运行时,和期望中的方法签名、参数以及参数具体的值一致的方法才会匹配期望(如果参数是对象,调用对象的equals来判别两个对象是否相同)。所以这是一种严格匹配。下面讲的是一种模糊匹配
“any”匹配(具体参数类型前面+any)
1 | CodeUnderTest cut; |
“with”匹配
withNotNull 不为null
withAny 任何的参数
withSubstring 任何包含某个字符串的字符串
1 |
|
指定执行次数的限制
记录或者验证阶段都可以使用该限制。
如果是记录了限制,那么在实际运行时超过限制将会出错
1 | CodeUnderTest cut; |
显示验证
在Verifications里面你可以使用类似于Expectations的语法来显示验证函数是否调用。注意可以加上”times=n”来限定调用的次数。
1 |
|
验证函数调用顺序:
1 |
|
Delegate:自定义结果(result, return)
适用于我们想根据函数调用的参数来决定函数运行的结果的场景
1 | CodeUnderTest cut; |
注意:Delegate是个空接口,只是用来告诉JMockit哪个调用要被delegated,方法的名字没有限制,同时参数可以匹配真实的调用也可以没有,另外delegate方法有一个默认的参数Invocation,它可以获取运行时调用实例的相关参数。返回的参数也可以和记录的方法不相同,但正常情况下应该都相同,为了防止后面的类型转换错误。
构造函数也可以被delegate
1 |
|
验证时获取调用参数
T withCapture() : 针对单词调用
T withCapture(List< T>) : 针对多次调用
List< T> withCapture(T) :针对构造函数(新创建的实例)
单次调用的参数获取:
1 |
|
withCpture()只能在verification块里使用,如果产生了多次调用,只会获取最后一次调用的信息
多次调用的参数获取:
withCapture(List)也可以使用在expectation中
1 |
|
获取构造函数的参数
1 |
|
级联Mock
针对这种obj1.getObj2(…).getYetAnotherObj().doSomething(…)级联调用的Mock。我们上面所讲的三个注解都可以实现这个功能
1 |
|
静态工厂方法的级联:
下面的方法不用关心FacesContext.getCurrentInstance(),因为jsf就是这个调用的返回值
1 |
|
对方法返回的是自身对象的级联Mock:
1 |
|
指定Mock的对象实例
@Mocked注解并不是指定哪个对象要被Mock,它是一种指定类型的Mock。因此如果指定某个对象被Mock,这时候就需要使用@Injectable注解,它是单例的,指定某一个实例被Mock。
注意:@Injectable Mock的实例是指传进来的或者是类里面注入的实例,不是调用构造函数创建的实例(因为是单例的,新创建的实例属于另一个对象了),另外静态方法( 静态方法属于类不属于对象)以及构造函数是不能被Mock的。
使用Injectable的好处就是单个定制化Mock对象的行为
1 | public final class ConcatenatingInputStream extends InputStream |
1 |
|
使用Mock注解来定制化Mock实例:
@Mocked和@Capturing注解其实也能指定对象,只要我们定义多个mock类属性或参数就行(有区分)
1 |
|
使用指定构造器(包括指定参数)创建的实例:
也就是指定在测试过程中创建的实例(测试前为创建)
需要注意的是,Expectations中定义的使用指定构造器构造的对象并不是一对一的映射,可以是多对一的映射(也就是 不是只生效一次)
1 |
|
另一种机制;
1 |
|
部分Mocking
jmockit默认所有的方法和构造函数都是被mock的,包括它的父类(除了Object)也会被Mock.但有时候我们只想部分Mock,不被Mock的部分仍按正常的逻辑执行。
案例:
Expectations可以传一个或多个类和Class,如果是传一个Class,该class中所有的方法和构造函数都可以被mocked(包括父类中的),该类的所有实例都会被认为是mock的对象。如果传的是对象,则只有方法(不包括构造函数)能被mock,而且仅有那个传的实例才能被mock。
注意:下面的这些方式有引出了另一种指定Mock对象类型的方式,也就是不通过类属性输入以及作为方法参数传入的方式来Mock某个类型
1 | public class PartialMockingTest |
当我们指定某个类或者某个类型被部分mock时,它仍能在验证阶段使用,即使没有record过
1 |
|
另外,还有一个最简单部分mock的方法,就是同时对mock的类属性实例使用@Tested和@Mocked注解
捕获实现类和实例
假设我们想测试的方式是基于某个接口的,也就是我们要mock该接口的实现类。但是有些实现类是通过匿名方式来创建的,比如下面的Service2。下面就是基于这种情况来讨论
1 | public interface Service { int doSomething(); } |
mock不确定的实现类,使用@Capturing注解,它能mock所有的实现类
1 | public final class UnitTest |
指定待创建类的行为:
maxInstances指定最大多少个实例被指定为和当前capture对象的行为一致。@Capturing会mock所有的实现类,不管有没有maxInstances注解,至于被mock对象的行为,可以通过maxInstances来控制
1 |
|
全验证和其他的验证法
全验证:也就是验证的步骤中少了一环,就会验证失败
1 |
|
验证某个调用没被调用:使用”times=0”
部分顺序的验证:
1 | DependencyAbc abc; |
1 |
|
排序的全验证:
1 |
|
针对某Mock对象的全验证:
1 |
|
验证没有调用发生:
注意下面的代码:为什么doSomething不在验证阶段统计?因为Expectations有doSomthing的期望,默认是被验证的,所以在Verifications里面被忽略。同时如果前面的验证也验证了某些方法,后面的全验证也会省略
1 |
|
验证未指定的调用不应该发生:
1 |
|