mock对象
虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
关于什么时候需要Mock对象,Tim Mackinnon给我们了一些建议:
真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
真实对象很难被创建(比如具体的web容器)
真实对象的某些行为很难触发(比如网络错误)
真实情况令程序的运行速度很慢
真实对象有用户界面
测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)
比如以下场景:
1. mock掉外部依赖的应用的HSF service的调用,比如uic,tp 的hsf服务依赖。
2. 对DAO层(访问MySQL、oracle、tair、tfs等底层存储)的调用mock等。
3. 对系统间异步交互notify消息的mock。
4. 对method_A里面调用到的method_B 的mock 。
5. 对一些应用里面自己的 class(abstract, final, static),interface,annotation ,enum,native等的mock。
Mock工具的原理:
1. record阶段:录制期望。也可以理解为数据准备阶段。创建依赖的class 或interface或method ,模拟返回的数据,及调用的次数等。
2. replay阶段:通过调用被测代码,执行测试。期间会invoke 到 第一阶段record的mock对象或方法。
3. verify阶段:验证。可以验证调用返回是否正确。及mock的方法调用次数,顺序等。
利用JMockit工具来编写基于行为的测试代码,通常符合下面的经典模板:
Jmockit的简介:
Jmockit可以mock的种类包含了:
1.class(abstract, final, static) ;
2.interface ;
3.enum ;
4.annotation ;
5.native 。
Jmockit 两种mock的方式:
一.根据用例的测试路径,测试代码内部逻辑Behavior-oriented(Expectations & Verifications)
对于这种情景,可以使用jmockit的基于行为的mock方式。目的是从被测代码的使用角度出发,结合数据的输入输出来检验程序运行的这个正确性。使用这个方式,需要把被依赖的代码mock掉,实际上相当于改变了被依赖的代码的逻辑。通常在集成测试中,如果有难以调用的外部接口,就通过这个方式mock掉,模拟外部接口。 这种方式有点像黑盒测试。
二.根据测试用例的输入输出数据,测试代码是否功能运行正常。State-oriented(MockUp<GenericType>)
对于这种情景,可以使用jmockit基于状态的mock方式。在这种方式中,目的是测试单元测试及其依赖代码的调用过程,验证代码逻辑是否满足测试路径。 由于被依赖代码可能在自己单测中已测试过,或者难以测试,就需要把这些被依赖代码的逻辑用预定期待的行为替换掉,也就是mock掉,从而把待测是代码隔离开,这也是单元测试的初衷。 这种方式和白盒测试接近。
通俗点讲,Behavior-oriented是基于行为的mock,对mock目标代码的行为进行模仿,更像黑盒测试。State-oriented 是基于状态的mock,是站在目标测试代码内部的。可以对传入的参数进行检查、匹配,才返回某些结果,类似白盒。而State-oriented的 new MockUp基本上可以mock任何代码或逻辑。非常强大。
JMockit元素
@Tested和@Injectable:
对@Tested对象判断是否为null,是则通过合适构造器初始化,并实现依赖注入。调用构造方法时,会尝试使用@Injectable的字段进行构造器注入。普通注入时,@Injectable字段如果没有在测试方法前被赋值,其行为将会被mock成默认值(静态方法和构造函数不会被mock掉)。Injectable最大作用除了注入,还有就是mock的范围只限当前注释实例。一句话:@Injectable的实例会自动注入到@Tested中,如果没初始赋值,那么JMockit将会以相应规则初始化。
@Mocked:
@Mocked修饰的实例,将会把实例对应类的所有实例的所有行为都mock掉(无论构造方法,还是private,protected方法,够霸气吧)。在Expectation区块中,声明的成员变量均默认带有@Mocked,@Mocked会mock掉所有方法,如果某些函数我们不希望它也被mock,可以通过methods="methodName"来设置被mock的类只对methodName方法进行mock。或者通过Expectation构造函数传入Class对象或Instance对象,这样只会区块内Class或Instance对应的行为进行mock。
比如:
在Expectation中没定义成员变量,而把CnServiceImpl.class显式地通过构造函数传入。这么做也是为了只对getBundleId方法mock,因为在Expectation构造函数传入Class对象或Instance对象后,只会区块内Class或Instance对应的行为进行mock。
1、字段,期望块的字段与期望块内的局部属性字段使用@Mocked来声明Mock类型。
2、参数,方法的参数声明来引入一个Mock类型。
第一种情况,属性字段是属于测试类或者一个mockit.Expectations子类(一个expectation期望块的内部的局部属性字段)。
第二种情况,参数必须是属于某个测试方法(@Test标签下的方法)。
在所有的情况,一个mock属性字段或者参数声明,都可以通过使用@Mocked声明。对于方法mock的参数或者在expectations期望块中定义的mock属性字段来说,该注解是可选的,而对于定义在测试类(XXXTest类)中的属性字段,@Mocked标签是必须,这是为了防止和该测试类的其它不需要mock的字段属性产生冲突。
Expectations:
Expectations是一个给定的单元测试相关的mock方法/构造函数的调用集合,是录制期望发生行为的地方。对于一个同样的方法或者构造函数,一个Expectations 可能会覆盖到多个不同调用,但是它不需要(不一定)覆盖到单元测试方法执行期间的所有调用(invocations)。一个特定的调用是否匹配给定的 expectation,不仅依赖方法/构造函数的签名,而且依赖运行时方面参数(aspects),例如被调用的方法类实例、参数值以及调用次数等等。 因此,对于给定的expectation,可以(可选)指定几种不同类型的匹配约束。result和times都是其内定成员变量。result可以重定义录制行为的返回值甚至通过Delegate来重定义行为,times是期待录制行为的发生次数。在Expectations中发生的调用,均会被mock。如果没定义result,方法调用的结果返回空。
对于一个返回值不为void类型的方法,Expectations中如何模拟方法返回值:
1)其返回值可以通过Expectations的result属性域来记录
2)Expectations的returns(Object)方法来记录
例如,方法返回一个Throwable异常类,只需将一个类型实验赋给result(注意,异常类只能通过result方式赋值,但是,在一些不常见的情况下面,有一个方法它实际就是返回一个异常或者错误对象时,我们就需要使用 returns(Object)方法来防止产生二义性。请注意,被抛出的异常/错误的记录,是适用于mock的方法(包括任何返回类型),以及mock的 构造函数。)。
JMockit也可以分类为非局部模拟与局部模拟,区分在于Expectations块是否有参数,有参数的是局部模拟,反之是非局部模拟。
而Expectations块一般由Expectations类和NonStrictExpectations类定义,类似于EasyMock和PowerMock中的Strict Mock和一般性Mock。
用Expectations类定义的,则mock对象在运行时只能按照 Expectations块中定义的顺序依次调用方法,不能多调用也不能少调用,所以可以省略掉Verifications块;
而用NonStrictExpectations类定义的,则没有这些限制,所以如果需要验证,则要添加Verifications块。
当这个方法在重播阶段调用时,这个被记录下来的特定的值将返回给调用者(通常情况下,这个调用者就是测试代码)。但 是,必须保证的是,在一个expectation期望块中,result的赋值或者returns(Object) 方在同一个expectation期望中,可以通过简单是对result属性域进行赋值,从而记录多个连续的结果(结果值包括返回值和抛出来的 throwable实例)。
多个返回值或者异常错误在记录阶段可以混合使用。对于记录多个连续的返回值的情况,形似returns(Object, Object...)这样的方法调用就可以满足了。同样,如果将一个包含了多个连续的值的列表list或者数据array赋值给result属性域,使用 一个result属性域也可以达到相同的效果。更多细节,可以参考相应的API文档
下面的例子展示了这样的情况:在UnitUnderTest记录阶段,对mock类DependencyAbc的方法同时记录了两种类型的返回结果。实现如下所示:法的调用,必须仅靠在记录阶段的方法调用所在地方的后面。
对于方法doSomething() 来说,一种可能的执行结果是在几个循环成功执行后,抛出SomeCheckedException异常。假设我们需要记录一个完整的期望集合(无论处于什 么原因),我们可能像下面这样编写测试代码。(通常情况下,对于mock的方法没有必要去指定所有可能的调用(invocations),也是不重要的, 特别是对于mock构造函数。
这里记录了三种不同的期望值。第一个(其实就是 DependencyAbc() 的构造函数调用)实际上会在测试代码中通过一个无参的构造函数来初始化这些依赖,对于这种调用是不需要任何返回值的,除非在构造函数里面抛出一个异常或者 错误(其实构造函数是没有返回值的,所以对它来说记录返回值是没什么意义可说)。第二个期望指定调用intReturningMethod()后将返回值 3。第三个期望就是,调用stringReturningMethod()方法后将按顺序返回3个连续的期望值,注意下,最后一个结果其实是一个需要检查 的异常实例,这样允许测试代码去到达它最初的目的
可伸缩的参数值匹配
给定一个测试用例,我们通常是不知道这些参数值到底是什么,或者这些参数对于测试的单元并不是必须的。所以,我们可以通过指定一个具有伸缩性(或者 说是灵活吧)参数匹配约束,而不是使用精确的参数值匹配约束,从而允许测试代码在重播阶段通过不同的参数值也可以匹配Record或者Verified阶 段声明的 期望调用集合。这个功能是通过使用withXyz(...)方法和 (或者)anyXyz域来实现。这些带有前缀"with"的方法 和前缀"any"的域都是定义在基类 mockit.Invocations里面。这个基类是mockit.Expectations和 mockit.Verifications的父类。因此,这些方法和域可以同时在Expectations和Verifications块中使用。
使用"with' 方法匹配参数
除了几个预定义的参数匹配的约束API,JMockit允许用户通过<T> T with(Object) and <T> T with(T, Object>) 这样的泛型方法来提供自定义的约束。参数Object类型可以是org.hamcrest.Matcher 对象(Hamcrest 库的对象),或者是一个合适的句柄对象
使用"any"属性字段匹配参数
总之,这里有两个参数匹配模式:一个基本的匹配是,没有任何的匹配约束指定所有参数必须是相等的;而另一个匹配是,存在一个匹配指定部分或全部参数 对应一个匹配的约束。但null值和上面的每个模式都不太一样,这可能会导致混乱。不过,对于涉及多个参数的复杂调用,可以使用"any"属性字段和 null引用的好处大于附加在API上的复杂性。
通过一个"可变参数(varargs )"参数来匹配值
有时,我们可能需要处理带有"可变参数"的方法或构造函数的期望。通过传递一个常规值来作为一个可变参数值是有效的,同样,使 用"with"、"any"匹配器来匹配也是有效的。然而,对于一个结合了两种值传递的相同期望,这并不是有效的。我们要么只能使用常规值或者参数匹配 值。
这种情况下,我们要匹配可变参数接收任何值(包括零)的调用,对这样的可变参数,我们可以指定一个期望使用个(Object[])any的约束来进行匹配。
也许最好的方式来理解可变参数匹配的确切语义(因为没有涉及特定的API)是阅读或实践实际的测试代码。这个 测试类 演示了几乎所有的可能性。
使用null值匹配任意对象引用
到目前为止,我们可以看出,一个expectation除了可以关联一个方法或者一个构造函数,还是可以指定调用的返回结果和参数匹配约束。在下面 这种情况下:在单元测试代码中,需要多次调用同一个方法或者构造函数,但其参数是不同的,此时,我们需要一种方法去声明期望去满足这些相互独立的调用。一 种方式是,就好像之前所见的,就是简单的为每一个调用声明一个独立的期望,声明顺序保持和调用执行时的顺序。另一种方式,对单个expectation期 望声明记录下两个或者更多个连续的返回结果。
而然,还存在另外一种方式,就是对一个给定的期望,指定该期望对应的调用执行次数的约束。为此,jmockit提供了三个特定的属性字段 域:times, minTimes和maxTimes。这些属性字段是属于mockit.Invocations类的,它是mockit.Expectations和 mockit.Verifications的一个非public的基类。因此,一个调用次数的约束可以用于记录阶段和检验阶段。在这几种情况下,与期望相 关联的方法或构造,在指定范围内将受到指定的调用次数的限制。一个调用如何少于期望的最少执行次数,或者超过期望的执行次数的上限,这时,单元测试会自动 失败。让我们看下面一些例子:
但不同于result属性字段的是,对于一个给定的期望,这三个属性字段最多只可以被指定一次。对于任何的调用次数的约束,一个非负整数都是有效 的。如果times=0或者maxTimes=0,那么在重播阶段(如果存在),发现存在一个调用能匹配上期望,则测试用例会因此失败。
Jmockit的实践:
第一步:添加jmockit的jar包依赖
现在假设我们有一个服务类能提供一下几种服务:1:查询数据库中的数据2:保存新建的对象到数据库中3:发送一封邮件到订阅者。前两个功能需要通过一个第三方的api库访问一个数据库。 第三个服务同样需要一个第三方的库来操作email程序。本例子中使用的是commons-email
所以这个例子中依赖了两个第三方的库,为了能能对这个服务进行单元测试我们需要通过一些Mock的api来模拟第三方的交互。
数据库类只包含静态方法和私有构造函数;查找和保存的方法我们不会在这里列出他们(假设他们是一个ORM API实现,如Hibernate或JPA)。
那么,如何才能不做任何改变现有的应用程序对“doBusinessOperationXyz”的方法进行单元测试? JMockit实际上提供了两种不同的Mock的API,每个有足够的强大能满足我们的需要。 (这个我们在前面有谈到,一个是行为导向Mock的API;另外一个,它可以被描述为一个面向状态的Mock的API)我们将在下面的看到如何使用他们。一个JUnit测试用例将验证从单元测试向它的外部依赖利息调用。这些调用的是那些在点(1) - (4)的上方。
使用Expectations API
首先,让我们先来看看JMockit Expectations API.
首先,先看下哪些字段可以被Mocked来修饰,本例中是测试类的实例字段标注为@Mocked。Mock字段可以是任何引用类型:一个接口,一个抽象类,final/ final类,enum类型、注释类型,甚至是一个泛型类型参数。我们所说的一个被模拟的域有一个mocke类型,其实就是指定的类型(比如@Mocked或者@Injection,@Tested等),一个mock 属性字段或者参数声明,都可以通过使用 mockit.Mocked注解(@Mocked)声明。对于方法mock的参数或者 在expectation 期望块中定义的mock属性字段来说,该注解是可选的。注解@Mocked(或者其他mock的注解类型,例如 @NonStrict)只是对于定义在测试类中的属性字段域才是必须的,这是为了防止和该测试类的其他不需要mock的字段属性产生冲突而已。
对于声明在测试方法的参数列表中的mock参数,当调用执行该测试方法时,Jmockit会对该声明类型的参数自动创建一个实例,并通过JUnit/TestNG 测试框架进行传递。因此这个参数值永远不会为null的。
对于mock属性字段域,Jmockit同样会自动创建一个实例,并设置到该属性字段域中,除非该字段域是一个final的域。对于这种情况,需要 在测试代码中显式的创建一个值并设置到域中。如果只有构造函数和静态方法将要调用它,那么这个域的值可以是null的,这样对于 mock的类来说也是有效的。
默认情况下,被mock的类型的所有方法在测试期间都被 mock实现,构造函数也是一样的。类初始化代码(在静态块和/或non-compile时间分配静态字段)会被取消,正如上面的数据库类。如果一个mock类型被声明为类,那么除了Java.lang.Object之外,该类的父类将被递归mock。因此,继承的方法也将自动 被mock。同样,对于声明为类的mock类型,其所有构造函数也将被 mock。甚至,无论方法或者构造函数的修饰符是否是private,stati,final,native等,这些方法和构造函数都会被mock掉,对 于mock类型来说,修饰符的定义变得如此不重要了。
在一个测试调用中,如果当一个方法或者构造函数被mock了,则其原始的实现代码将不会被执行,取而代之的是,可以通过 jmockit显式或者隐式指定测试调用。
(注意JMockit不创建任何要模拟对象的子类;它直接修改mock类的实际实现。换句话说,一个接口方法和抽象方法可以不需要有任何实现代码。)
使用Verifications API
前面我们说的都是模拟对象和录制行为,接下来我们来看看如何校验非严格的执行结果
Expectations块一般由Expectations类和NonStrictExpectations类定义,类似于EasyMock和PowerMock中的Strict Mock和一般性Mock。
用Expectations类定义的,则mock对象在运行时只能按照 Expectations块中定义的顺序依次调用方法,不能多调用也不能少调用,所以可以省略掉Verifications块;而用NonStrictExpectations类定义的,则没有这些限制,所以如果需要验证,则要添加Verifications块。
在上面的例子中,对于第三方的封装的类进行mock之后,在Expectations体中录制方法调用的行为和结果,然后在调用要测试的方法,例子中用到了NonStrictExpectations和Expectations两种期望,第一种是非严格的,里面录制的代码不一定都执行,你可以在调用完测试方法后中的Verifications进行校验调用次数,而Expectations则默认自动校验。当我们使用的非严格的期望,在回放阶段调用mock方法和构造函数不会立即验证(除非显式指定否则通过调用数约束)。那些记录的非严格调用与一个特定的返回值或/错误抛出一个异常会产生预期的结果如果重播生产代码。
非严格期望只是证明测试单元做了正确的事情。例如,在上面的第二个测试假设记录Database.find(…)的行调用被注释掉了。测试会失败,另一部分的代码在测试取决于执行返回值时,或者当一个预期调用验证测试本身(在这个例子测试中,一个额外的email.setMsg(withNotEqual(" ")))在需要验证的其他两个验证调用之间),在某些情况下,您可能希望确保调用至少会发生一次。可以简单地指定它minTimes = 1约束非严格记录调用后的期望。
Using the Mockups API
上面的例子中,已经不是通过调用mocked类型记录或验证预期,我们直接指定感兴趣的mock的实现方法和构造函数。待模拟方法必须具有相同的方法和构造函数,并用@Mock注释。他们是定义在一个模拟类中,它可以是在一个定义测试方法的一个单独的类(嵌套)或一个匿名内部类;在这两种情况下,它必须扩展通用模型< T >基类,同时提供类型被mock为类型参数T的“value”。 上面的两个测试共享一个可重用的模拟类,MockDatabase,应用到测试类作为一个整体在一个@BeforeClass方法。请注意,我们还阻止数据库类静态初始化,通过定义特殊的模拟方法”clinit$()”。这是必要的,在这种情况下,因为数据库类实际上创建了一个会在其静态初始化实例。 每个测试设置特定的模拟电子邮件类通过创建内联(匿名)模型类实例。见这些模拟方法,@Mock注释可以选择性地指定确切/最小/最大限制预期/允许调用相应的实际方法。尽管这里没有显示,构造函数可以mock与模拟方法命名为“init$”和有相同的参数构造函数被mock了。