Jmockit使用指南

来源:互联网 发布:java接口开发demo下载 编辑:程序博客网 时间:2024/05/16 12:10

Jmockit使用指南


目录(?)[+]

概述

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工具来编写基于行为的测试代码,通常符合下面的经典模板:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. import mockit.*;    
  2. ... other imports ...    
  3.      
  4. public class SomeTest    
  5. {    
  6.    //  零个或者更多的mock 属性, 这些属性对于整个类的所有测试方法来说是通用的。    
  7.    @Mocked Collaborator mockCollaborator;    
  8.    @NonStrict AnotherDependency anotherDependency;    
  9.    ...    
  10.      
  11.    @Test    
  12.    public void testWithRecordAndReplayOnly(mock parameters)    
  13.    {    
  14.       // 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。    
  15.      
  16.       new Expectations() // 一个期望块    
  17.       {    
  18.          // 零个或者多个局部 mock 属性域    
  19.      
  20.          {    
  21.             //  一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来    
  22.             //一些没有被mock的方法、对象类型等同样可以在这个期望块里面调用    
  23.          }    
  24.       };    
  25.      
  26.       // 单元测试代码真正业务逻辑在此执行    
  27.      
  28.       // 如果需要,可以在这里进行验证代码编写,当然可以利用JUnit/TestNG 断言    
  29.    }    
  30.      
  31.    @Test    
  32.    public void testWithReplayAndVerifyOnly(mock parameters)    
  33.    {    
  34.       // 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。    
  35.      
  36.       // 单元测试代码真正业务逻辑在此执行    
  37.      
  38.       new Verifications() {{ // 一个验证块    
  39.           //  一个或者多个mock对象(类型)的调用,这些调用用于验证结果是否正确    
  40.             //一些没有被mock的方法、对象类型等同样可以在这个验证块里面调用    
  41.       }};    
  42.      
  43.       // 如果需求,这里可以添加其他额外的验证代码,    
  44.      //  当然,这些验证可以编写在这里,也可以在Verifications块之前    
  45.    }    
  46.      
  47.    @Test    
  48.    public void testWithBothRecordAndVerify(mock parameters)    
  49.    {    
  50.       //如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。    
  51.      
  52.       new NonStrictExpectations() { // 同样是一个期望块    
  53.          //零个或者多个局部 mock 属性域    
  54.          {    
  55.             // 一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来    
  56.          }    
  57.       };    
  58.      
  59.       // 单元测试代码真正业务逻辑在此执行    
  60.      
  61.       new VerificationsInOrder() {{ // 同样是一个验证块    
  62.          // 一个或者多个mock对象(类型)的调用,这些调用将期望按照特定的顺序进行比较。    
  63.       }};    
  64.      
  65.      // 如果需求,这里可以添加其他额外的验证代码,    
  66.      //  当然,这些验证可以编写在这里,也可以在Verifications块之前    
  67.    }    


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。

比如:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.   @Test    
  2.     public void behaviorTest_fail3time() {    
  3.                  
  4.         new Expectations() {        // Expectations中包含的内部类区块中,体现的是一个录制被测类的逻辑。    
  5.             @Mocked(methods="tryIt")  // 表明被修饰的类对tryIt()方法进行mock。    
  6.             Guess g;    
  7.             {    
  8.                 g.tryIt();             // 期待调用Guess.tryIt()方法    
  9.                 result = false;        // mock掉返回值为false(表明猜不中)    
  10.                 times = 3;             // 期待以上过程重复3次    
  11.                 guessDAO.saveResult(false, anyInt); // 期待调用guessDAO把猜失败的结果保存    
  12.             }    
  13.         };    
  14.         guess.doit();               // 录制完成后,进行实际的代码调用,也称回放(replay)    
  15.     }    
  16. }    
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class Test {  
  2.   @Mocked  
  3.   private Flow flow;   
  4.   @Tested  
  5.   private CServiceImpl cService = new CServiceImpl();  
  6.   @org.junit.Test  
  7.   public void test(@Mocked Flow flow) {  
  8.     new  Expectations(CnServiceImpl.class){  
  9.       {  
  10.          System.out.println(cService.getBundleId());  
  11.       }  
  12.     };  
  13.   }  
  14. }  
在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的方法同时记录了两种类型的返回结果。实现如下所示:法的调用,必须仅靠在记录阶段的方法调用所在地方的后面。

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class UnitUnderTest  
  2. {  
  3. (1)private final DependencyAbc abc = new DependencyAbc();  
  4.    
  5.    public void doSomething()  
  6.    {  
  7. (2)   int n = abc.intReturningMethod();  
  8.    
  9.       for (int i = 0; i < n; i++) {  
  10.          String s;  
  11.    
  12.          try {  
  13. (3)         s = abc.stringReturningMethod();  
  14.          }  
  15.          catch (SomeCheckedException e) {  
  16.             // 处理异常  
  17.          }  
  18.    
  19.          // 这里可以处理其他逻辑  
  20.       }  
  21.    }  
  22. }  
对于方法doSomething() 来说,一种可能的执行结果是在几个循环成功执行后,抛出SomeCheckedException异常。假设我们需要记录一个完整的期望集合(无论处于什 么原因),我们可能像下面这样编写测试代码。(通常情况下,对于mock的方法没有必要去指定所有可能的调用(invocations),也是不重要的, 特别是对于mock构造函数。
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Test  
  2.    public void doSomethingHandlesSomeCheckedException() throws Exception  
  3.    {  
  4.       new Expectations() {  
  5.          DependencyAbc abc;  
  6.    
  7.          {  
  8. (1)         new DependencyAbc();  
  9.    
  10. (2)         abc.intReturningMethod(); result = 3;  
  11.    
  12. (3)         abc.stringReturningMethod();  
  13.             returns("str1""str2");  
  14.             result = new SomeCheckedException();  
  15.          }  
  16.       };  
  17.    
  18.       new UnitUnderTest().doSomething();  
  19.    }  

这里记录了三种不同的期望值。第一个(其实就是 DependencyAbc() 的构造函数调用)实际上会在测试代码中通过一个无参的构造函数来初始化这些依赖,对于这种调用是不需要任何返回值的,除非在构造函数里面抛出一个异常或者 错误(其实构造函数是没有返回值的,所以对它来说记录返回值是没什么意义可说)。第二个期望指定调用intReturningMethod()后将返回值 3。第三个期望就是,调用stringReturningMethod()方法后将按顺序返回3个连续的期望值,注意下,最后一个结果其实是一个需要检查 的异常实例,这样允许测试代码去到达它最初的目的

可伸缩的参数值匹配

在记录和验证阶段,一个mock方法或者构造函数的调用标示一个expectation期望。如果方法/构造函数具有一个或者多个参数,那么一个记录/验 证的expectation期望格式类似:doSomething(1, "s", true);如果在重播阶段存在一个调用,它具有相同(equal)的参数值,那么这次调用将匹配该期望。对于一般的对象(不是原生的对象或者数组),它 的 equals(Object) 方法将用在相等性的检查。对于数组类型参数,相等性取决于各个独立的元素的相等性。因此,两个不同的数组实例,在每一维(译者注:数组可能存在多维数组的 情况)必须具有相同的长度,而且按顺序比较各个元素的相等(利用equals(Object)方法),才能认为两个数组实例相等。
给定一个测试用例,我们通常是不知道这些参数值到底是什么,或者这些参数对于测试的单元并不是必须的。所以,我们可以通过指定一个具有伸缩性(或者 说是灵活吧)参数匹配约束,而不是使用精确的参数值匹配约束,从而允许测试代码在重播阶段通过不同的参数值也可以匹配Record或者Verified阶 段声明的 期望调用集合。这个功能是通过使用withXyz(...)方法和 (或者)anyXyz域来实现。这些带有前缀"with"的方法 和前缀"any"的域都是定义在基类 mockit.Invocations里面。这个基类是mockit.Expectations和 mockit.Verifications的父类。因此,这些方法和域可以同时在Expectations和Verifications块中使用。

使用"with' 方法匹配参数
当录制或者校验一个期望时,调用withXyz(...)方法可以在产生任意的参数子集,这些参数是通过调用进行传递。它们和常规的参数传递(使用具体 值、局部变量等)可以一起自由混合使用。唯一需要满足的是,这些调用必须出现录制/校验调用语句里面,而不是前面。这是不可能的,例如,先分配 withNotEqual(VAL)的调用结果到一个局部变量,然后在调用语句中使用这个变量。下面是一个测试例子,使用一些"with"的方法。 
当录制或者校验一个期望时,调用withXyz(...)方法可以在产生任意的参数子集,这些参数是通过调用进行传递。它们和常规的参数传递(使用具体 值、局部变量等)可以一起自由混合使用。唯一需要满足的是,这些调用必须出现录制/校验调用语句里面,而不是前面。这是不可能的,例如,先分配 withNotEqual(VAL)的调用结果到一个局部变量,然后在调用语句中使用这个变量。下面是一个测试例子,使用一些"with"的方法。 
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Test  
  2.    public void someTestMethod(@NonStrict final DependencyAbc abc)  
  3.    {  
  4.       final DataItem item = new DataItem(...);  
  5.    
  6.       new Expectations() {{  
  7.          // 那些第一个参数等于"str"而且第二个参数不为null的调用将匹配"voidMethod(String, List)"方法.  
  8.          abc.voidMethod("str", (List<?>) withNotNull());  
  9.    
  10.          //对于类 DependencyAbc的实例,如果调用的stringReturningMethod(DataItem, String)的方法,  
  11.         //满足第一个参数指针指向同一个"item",而且第二个参数俺有字符串 "xyz".那么该次调用将匹配下面的期望  
  12.          abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));  
  13.       }};  
  14.    
  15.       new UnitUnderTest().doSomething(item);  
  16.    
  17.       new Verifications() {{  
  18.          // 匹配所有参数为任何long类型的方法调用.  
  19.          abc.anotherVoidMethod(withAny(1L));  
  20.       }};  
  21.    }  

除了几个预定义的参数匹配的约束API,JMockit允许用户通过<T> T with(Object) and <T> T with(T, Object>) 这样的泛型方法来提供自定义的约束。参数Object类型可以是org.hamcrest.Matcher 对象(Hamcrest 库的对象),或者是一个合适的句柄对象

使用"any"属性字段匹配参数
最常见的参数匹配约束往往就是限制条件最少的那种匹配约束:匹配任何一个给定的参数(当然是正确的参数类型)的值调用。为此,我们有一个参数匹配的 特殊的属性字段集合,一些是为匹配所有的原始类型(包括相应的包装类),一些是用于字符串,以及一些用于匹配"通用"类型的对象。下面的测试演示了一些使 用。
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Test  
  2.    public void someTestMethod(@NonStrict final DependencyAbc abc)  
  3.    {  
  4.       final DataItem item = new DataItem(...);  
  5.    
  6.       new Expectations() {{  
  7.          // 这里期望的声明,会匹配所有这样的"voidMethod(String, List)"方法调用:  
  8.         // 其第一个参数为任意字符串,而且第二个参数是任何一个list实例对象  
  9.          abc.voidMethod(anyString, (List<?>) any);  
  10.       }};  
  11.    
  12.       new UnitUnderTest().doSomething(item);  
  13.    
  14.       new Verifications() {{  
  15.          // 匹配参数类型是long或者Long的方法调用  
  16.          abc.anotherVoidMethod(anyLong);  
  17.       }};  
  18.    }  
当一个或者多个参数匹配同时使用时,而且对于给定的参数必须匹配null引用,那么withNull()就应该被使用。
总之,这里有两个参数匹配模式:一个基本的匹配是,没有任何的匹配约束指定所有参数必须是相等的;而另一个匹配是,存在一个匹配指定部分或全部参数 对应一个匹配的约束。但null值和上面的每个模式都不太一样,这可能会导致混乱。不过,对于涉及多个参数的复杂调用,可以使用"any"属性字段和 null引用的好处大于附加在API上的复杂性。
通过一个"可变参数(varargs )"参数来匹配值
有时,我们可能需要处理带有"可变参数"的方法或构造函数的期望。通过传递一个常规值来作为一个可变参数值是有效的,同样,使 用"with"、"any"匹配器来匹配也是有效的。然而,对于一个结合了两种值传递的相同期望,这并不是有效的。我们要么只能使用常规值或者参数匹配 值。
这种情况下,我们要匹配可变参数接收任何值(包括零)的调用,对这样的可变参数,我们可以指定一个期望使用个(Object[])any的约束来进行匹配。
也许最好的方式来理解可变参数匹配的确切语义(因为没有涉及特定的API)是阅读或实践实际的测试代码。这个测试类 演示了几乎所有的可能性。
使用null值匹配任意对象引用
对于给定的一个期望,当我们需要使用至少一个参数匹配方法或者字段时,我们可以使用一个 "便捷"的方式去指定该期望接受所有任意的对象引用(引用类型的参数),只需使用一个null值,而不是使用withAny(X)或"any'属性字段, 特别是,这样可以不需要将值的类型转换为参数声明时的类型。然而,需要记住的是,这种行为只适合这样的期望,它使用了至少一个显式的参数匹配(或 是"with"方法,或是"any"属性字段)。当null值传递给一个没有任何匹配的调用时,空值将只匹配空引用。在前面的测试,因此我们可以这样写:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Test  
  2.    public void someTestMethod(@NonStrict final DependencyAbc abc)  
  3.    {  
  4.       ...  
  5.    
  6.       new Expectations() {{  
  7.          abc.voidMethod(anyString, null);  
  8.       }};  
  9.    
  10.       ...  
  11.    }  

到目前为止,我们可以看出,一个expectation除了可以关联一个方法或者一个构造函数,还是可以指定调用的返回结果和参数匹配约束。在下面 这种情况下:在单元测试代码中,需要多次调用同一个方法或者构造函数,但其参数是不同的,此时,我们需要一种方法去声明期望去满足这些相互独立的调用。一 种方式是,就好像之前所见的,就是简单的为每一个调用声明一个独立的期望,声明顺序保持和调用执行时的顺序。另一种方式,对单个expectation期 望声明记录下两个或者更多个连续的返回结果。
而然,还存在另外一种方式,就是对一个给定的期望,指定该期望对应的调用执行次数的约束。为此,jmockit提供了三个特定的属性字段 域:times, minTimes和maxTimes。这些属性字段是属于mockit.Invocations类的,它是mockit.Expectations和 mockit.Verifications的一个非public的基类。因此,一个调用次数的约束可以用于记录阶段和检验阶段。在这几种情况下,与期望相 关联的方法或构造,在指定范围内将受到指定的调用次数的限制。一个调用如何少于期望的最少执行次数,或者超过期望的执行次数的上限,这时,单元测试会自动 失败。让我们看下面一些例子:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Test  
  2.    public void someTestMethod(final DependencyAbc abc)  
  3.    {  
  4.       new Expectations() {{  
  5.          // By default, one invocation is expected, i.e. "times = 1":  
  6.          new DependencyAbc();  
  7.    
  8.          // At least two invocations are expected:  
  9.          abc.voidMethod(); minTimes = 2;  
  10.    
  11.          // 1 to 5 invocations are expected:  
  12.          abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;  
  13.       }};  
  14.    
  15.       new UnitUnderTest().doSomething();  
  16.    }  
  17.    
  18.    @Test  
  19.    public void someOtherTestMethod(final DependencyAbc abc)  
  20.    {  
  21.       new UnitUnderTest().doSomething();  
  22.    
  23.       new Verifications() {{  
  24.          // Verifies that zero or one invocations occurred, with the specified argument value:  
  25.          abc.anotherVoidMethod(3); maxTimes = 1;  
  26.    
  27.          // Verifies the occurrence of at least one invocation with the specified arguments:  
  28.          DependencyAbc.someStaticMethod("test"false); // "minTimes = 1" is implied  
  29.       }};  
  30.    }  

但不同于result属性字段的是,对于一个给定的期望,这三个属性字段最多只可以被指定一次。对于任何的调用次数的约束,一个非负整数都是有效 的。如果times=0或者maxTimes=0,那么在重播阶段(如果存在),发现存在一个调用能匹配上期望,则测试用例会因此失败。

Jmockit的实践:

第一步:添加jmockit的jar包依赖 

[html] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <dependency>    
  2.             <groupId>com.googlecode.jmockit</groupId>    
  3.             <artifactId>jmockit</artifactId>    
  4.             <version>1.5</version>    
  5.             <scope>test</scope>    
  6.         </dependency>    
  7.         <dependency>    
  8.             <groupId>com.googlecode.jmockit</groupId>    
  9.             <artifactId>jmockit-coverage</artifactId>    
  10.             <version>0.999.24</version>    
  11.             <scope>test</scope>    
  12.         </dependency>    

现在假设我们有一个服务类能提供一下几种服务:1:查询数据库中的数据2:保存新建的对象到数据库中3:发送一封邮件到订阅者。前两个功能需要通过一个第三方的api库访问一个数据库。 第三个服务同样需要一个第三方的库来操作email程序。本例子中使用的是commons-email

所以这个例子中依赖了两个第三方的库,为了能能对这个服务进行单元测试我们需要通过一些Mock的api来模拟第三方的交互。 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package jmockit.tutorial.domain;  
  2.   
  3. import java.math.*;  
  4. import java.util.*;  
  5. import org.apache.commons.mail.*;  
  6. import static jmockit.tutorial.persistence.Database.*;  
  7.   
  8. public final class MyBusinessService  
  9. {  
  10.    public void doBusinessOperationXyz(EntityX data) throws EmailException  
  11.    {  
  12.       List<EntityX> items =  
  13. (1)      find("select item from EntityX item where item.someProperty = ?1", data.getSomeProperty());  
  14.   
  15.       // Compute or obtain from another service a total value for the new persistent entity:  
  16.       BigDecimal total = ...  
  17.       data.setTotal(total);  
  18.   
  19. (2)   persist(data);  
  20.   
  21.       sendNotificationEmail(data, items);  
  22.    }  
  23.   
  24.    private void sendNotificationEmail(EntityX data, List<EntityX> items) throws EmailException  
  25.    {  
  26.       Email email = new SimpleEmail();  
  27.       email.setSubject("Notification about processing of ...");  
  28. (3)   email.addTo(data.getCustomerEmail());  
  29.   
  30.       // Other e-mail parameters, such as the host name of the mail server, have defaults defined  
  31.       // through external configuration.  
  32.   
  33.       String message = buildNotificationMessage(items);  
  34.       email.setMsg(message);  
  35.   
  36. (4)   email.send();  
  37.    }  
  38.   
  39.    private String buildNotificationMessage(List<EntityX> items) { ... }  
  40. }  

数据库类只包含静态方法和私有构造函数;查找和保存的方法我们不会在这里列出他们(假设他们是一个ORM API实现,如Hibernate或JPA)。

那么,如何才能不做任何改变现有的应用程序对“doBusinessOperationXyz”的方法进行单元测试? JMockit实际上提供了两种不同的Mock的API,每个有足够的强大能满足我们的需要。 (这个我们在前面有谈到,一个是行为导向Mock的API;另外一个,它可以被描述为一个面向状态的Mock的API)我们将在下面的看到如何使用他们。一个JUnit测试用例将验证从单元测试向它的外部依赖利息调用。这些调用的是那些在点(1) - (4)的上方。

使用Expectations API

首先,让我们先来看看JMockit Expectations API.

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package jmockit.tutorial.domain;  
  2.   
  3. import org.apache.commons.mail.*;  
  4. import jmockit.tutorial.persistence.*;  
  5.   
  6. import org.junit.*;  
  7. import mockit.*;  
  8.   
  9. public final class MyBusinessService_ExpectationsAPI_Test  
  10. {  
  11.    @Mocked(stubOutClassInitialization = truefinal Database unused = null;  
  12.    @Mocked SimpleEmail email;  
  13.   
  14.    @Test  
  15.    public void doBusinessOperationXyz() throws Exception  
  16.    {  
  17.       final EntityX data = new EntityX(5"abc""abc@xpta.net");  
  18.   
  19.       // Recorded strictly, so matching invocations must be replayed in the same order:  
  20.       new Expectations() {{  
  21. (1)      Database.find(withSubstring("select"), any);  
  22.          result = new EntityX(1"AX5""someone@somewhere.com");  
  23.   
  24. (2)      Database.persist(data);  
  25.       }};  
  26.   
  27.       // Recorded non-strictly, so matching invocations can be replayed in any order:  
  28.       new NonStrictExpectations() {{  
  29. (4)      email.send(); times = 1// a non-strict invocation requires a constraint if expected  
  30.       }};  
  31.   
  32.       new MyBusinessService().doBusinessOperationXyz(data);  
  33.    }  
  34.   
  35.    @Test(expected = EmailException.class)  
  36.    public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception  
  37.    {  
  38.       new NonStrictExpectations() {{  
  39. (3)      email.addTo((String) withNotNull()); result = new EmailException();  
  40.   
  41.          // If the e-mail address is invalid, sending the message should not be attempted:  
  42.          email.send(); times = 0;  
  43.       }};  
  44.   
  45.       EntityX data = new EntityX(5"abc""someone@somewhere.com");  
  46.       new MyBusinessService().doBusinessOperationXyz(data);  
  47.    }  
  48. }  

首先,先看下哪些字段可以被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

前面我们说的都是模拟对象和录制行为,接下来我们来看看如何校验非严格的执行结果

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package jmockit.tutorial.domain;  
  2.   
  3. import org.apache.commons.mail.*;  
  4. import jmockit.tutorial.persistence.*;  
  5.   
  6. import org.junit.*;  
  7. import mockit.*;  
  8.   
  9. public final class MyBusinessService_VerificationsAPI_Test  
  10. {  
  11.    @Tested MyBusinessService service; // instantiated automatically  
  12.    @Mocked(stubOutClassInitialization = true) Database onlyStatics;  
  13.    @Capturing Email email; // concrete subclass mocked on demand, when loaded  
  14.   
  15.    final EntityX data = new EntityX(5"abc""someone@somewhere.com");  
  16.   
  17.    @Test  
  18.    public void doBusinessOperationXyzPersistsData() throws Exception  
  19.    {  
  20.       // No expectations recorded in this case.  
  21.   
  22.       service.doBusinessOperationXyz(data);  
  23.   
  24. (2)   new Verifications() {{ Database.persist(data); }};  
  25.    }  
  26.   
  27.    @Test  
  28.    public void doBusinessOperationXyzFindsItemsAndSendsNotificationEmail() throws Exception  
  29.    {  
  30.       // Invocations that produce a result are recorded, but only those we care about.  
  31.       new NonStrictExpectations() {{  
  32. (1)      Database.find(withSubstring("select"), (Object[]) null);  
  33.          result = new EntityX(1"AX5""someone@somewhere.com");  
  34.       }};  
  35.   
  36.       service.doBusinessOperationXyz(data);  
  37.   
  38.       new VerificationsInOrder() {{  
  39. (3)      email.addTo(data.getCustomerEmail());  
  40. (4)      email.send();  
  41.       }};  
  42.    }  
  43. }  
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

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package jmockit.tutorial.domain;  
  2.   
  3. import java.util.*;  
  4. import org.apache.commons.mail.*;  
  5. import jmockit.tutorial.persistence.*;  
  6.   
  7. import static org.junit.Assert.*;  
  8. import org.junit.*;  
  9. import mockit.*;  
  10.   
  11. public final class MyBusinessService_MockupsAPI_Test  
  12. {  
  13.    public static final class MockDatabase extends MockUp<Database>  
  14.    {  
  15.       @Mock  
  16.       public void $clinit() { /* do nothing */ }  
  17.   
  18.       @Mock(invocations = 1)  
  19. (1)   public List<EntityX> find(String ql, Object... args)  
  20.       {  
  21.          assertNotNull(ql);  
  22.          assertTrue(args.length > 0);  
  23.          return Arrays.asList(new EntityX(1"AX5""someone@somewhere.com"));  
  24.       }  
  25.      
  26.       @Mock(maxInvocations = 1)  
  27. (2)   public void persist(Object o) { assertNotNull(o); }  
  28.    }  
  29.   
  30.    @BeforeClass  
  31.    public static void mockUpPersistenceFacade()  
  32.    {  
  33.       // Applies the mock class by invoking its constructor:  
  34.       new MockDatabase();  
  35.    }  
  36.   
  37.    final EntityX data = new EntityX(5"abc""5453-1");  
  38.   
  39.    @Test  
  40.    public void doBusinessOperationXyz() throws Exception  
  41.    {  
  42.       // Defines and applies a mock class in one operation:  
  43.       new MockUp<Email>() {  
  44.          @Mock(invocations = 1)  
  45.          Email addTo(Invocation inv, String email)  
  46.          {  
  47.             assertEquals(data.getCustomerEmail(), email);  
  48.             return inv.getInvokedInstance();  
  49.          }  
  50.   
  51.          @Mock(invocations = 1)  
  52. (4)      String send() { return ""; }  
  53.       };  
  54.      
  55.       new MyBusinessService().doBusinessOperationXyz(data);  
  56.    }  
  57.   
  58.    @Test(expected = EmailException.class)  
  59.    public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception  
  60.    {  
  61.       new MockUp<Email>() {  
  62.          @Mock  
  63. (3)      Email addTo(String email) throws EmailException  
  64.          {  
  65.             assertNotNull(email);  
  66.             throw new EmailException();  
  67.          }  
  68.         
  69.          @Mock(invocations = 0)  
  70.          String send() { return null; }  
  71.       };  
  72.      
  73.       new MyBusinessService().doBusinessOperationXyz(data);  
  74.    }  
  75. }  

上面的例子中,已经不是通过调用mocked类型记录或验证预期,我们直接指定感兴趣的mock的实现方法和构造函数。待模拟方法必须具有相同的方法和构造函数,并用@Mock注释。他们是定义在一个模拟类中,它可以是在一个定义测试方法的一个单独的类(嵌套)或一个匿名内部类;在这两种情况下,它必须扩展通用模型< T >基类,同时提供类型被mock为类型参数T的“value”。
上面的两个测试共享一个可重用的模拟类,MockDatabase,应用到测试类作为一个整体在一个@BeforeClass方法。请注意,我们还阻止数据库类静态初始化,通过定义特殊的模拟方法”clinit$()”。这是必要的,在这种情况下,因为数据库类实际上创建了一个会在其静态初始化实例。
每个测试设置特定的模拟电子邮件类通过创建内联(匿名)模型类实例。见这些模拟方法,@Mock注释可以选择性地指定确切/最小/最大限制预期/允许调用相应的实际方法。尽管这里没有显示,构造函数可以mock与模拟方法命名为“init$”和有相同的参数构造函数被mock了。
0 0