可以通过一个实例属性字段或者参数声明来引入一个Mock类型。在第一种情况,属性字段是属于测试类或者一个mockit.Expectations 子类(一个expectation 期望块的内部的局部属性字段)。第二种情况,参数必须是属于某个测试方法。
在所有情况,一个mock 属性字段或者参数声明,都可以通过使用 mockit.Mocked注解(@Mocked)声明。对于方法mock的参数或者 在expectation 期望块中定义的mock属性字段来说,该注解是可选的。注解@Mocked(或者其他mock的注解类型,例如 @NonStrict)只是对于定义在测试类中的属性字段域才是必须的,这是为了防止和该测试类的其他不需要mock的字段属性产生冲突而已。
Java类型的各种mock属性字段域和参数都是有效的,除了基本类型和数组类型。所以,以下引用类型是有效的:接口,具体的类,抽象 类,final类,枚举类型,注释类型。请注意,这包括从JRE的引用类型(属于标准的包之一,例如java.lang,java.util,等等依此类 推)。
对于声明在测试方法的参数列表中的mock参数,当调用执行该测试方法时,Jmockit会对该声明类型的参数自动创建一个实例,并通过JUnit/TestNG 测试框架进行传递。因此这个参数值永远不会为null的。
对于mock属性字段域,Jmockit同样会自动创建一个实例,并设置到该属性字段域中,除非该字段域是一个final的域。对于这种情况,需要 在测试代码中显式的创建一个值并设置到域中。如果只有构造函数和静态方法将要调用它,那么这个域的值可以是null的,这样对于 mock的类来说也是有效的。
最后,我们注意到,使用Jmockit来创建mock实例,其实与使用其他传统的mock工具来创建mock对象是很类似的,但是又不完全一样。对 于每一个mock类型实例(无论是使用jmockit或者其他工具来创建),都将被mock模拟了,只要这些类型仍然被mocked。对于传统的mock api来说,只有被mock工具创建的mock实例才真正被mock掉。但在大多数的测试代码编写过程中,这些只是有很少的区别,甚至说,没有任何区别, 只是我们需要在脑瓜中记住这些差异就行。
public interface Dependency // an arbitrary custom interface { String doSomething(boolean b); } public final class MultiMocksTest<MultiMock extends Dependency & Runnable> { @Mocked MultiMock multiMock; @Test public void mockFieldWithTwoInterfaces() { new NonStrictExpectations() {{ multiMock.doSomething(false); result = "test"; }}; multiMock.run(); assertEquals("test", multiMock.doSomething(false)); new Verifications() {{ multiMock.run(); }}; } @Test public <M extends Dependency & Serializable> void mockParameterWithTwoInterfaces(final M mock) { new Expectations() {{ mock.doSomething(true); result = "" }}; assertEquals("", mock.doSomething(true)); } }
从上面可以看出来,两个接口同时被mocked了:Dependency和java.lang.Runnable作为一个mock属性域,同时 Dependency和 java.io.Serializable作为参数进行mock。我们通过声明类型变量MultiMock(它的作用域是整个测试类)和M(它的作用域是 单个测试方法),从而使得Jmockit在各种情况下知道这些接口的信息。
_如果这个测试需要获取一个异常(exception)或者错误(error)时,_result属性域同样可以使用。很简单,此时只需要将一个 throwable实例赋值给它就可以了。但是,在一些不常见的情况下面,有一个方法它实际就是返回一个异常或者错误对象时,我们就需要使用 returns(Object)方法来防止产生二义性。请注意,被抛出的异常/错误的记录,是适用于mock的方法(包括任何返回类型),以及mock的 构造函数。
在同一个expectation期望中,可以通过简单是对result属性域进行赋值,从而记录多个连续的结果(结果值包括返回值和抛出来的 throwable实例)。多个返回值或者异常错误在记录阶段可以混合使用。对于记录多个连续的返回值的情况,形似returns(Object, Object...)这样的方法调用就可以满足了。同样,如果将一个包含了多个连续的值的列表list或者数据array赋值给result属性域,使用 一个result属性域也可以达到相同的效果。更多细节,可以参考相应的API文档
下面的例子展示了这样的情况:在UnitUnderTest记录阶段,对mock类DependencyAbc的方法同时记录了两种类型的返回结果。实现如下所示:
public class UnitUnderTest { (1)private final DependencyAbc abc = new DependencyAbc(); public void doSomething() { (2) int n = abc.intReturningMethod(); for (int i = 0; i < n; i++) { String s; try { (3) s = abc.stringReturningMethod(); } catch (SomeCheckedException e) { // 处理异常 } // 这里可以处理其他逻辑 } } }
对于方法doSomething() 来说,一种可能的执行结果是在几个循环成功执行后,抛出SomeCheckedException异常。假设我们需要记录一个完整的期望集合(无论处于什 么原因),我们可能像下面这样编写测试代码。(通常情况下,对于mock的方法没有必要去指定所有可能的调用(invocations),也是不重要的, 特别是对于mock构造函数。我们迟点会讨论这个话题)。
@Test public void doSomethingHandlesSomeCheckedException() throws Exception { new Expectations() { DependencyAbc abc; { (1) new DependencyAbc(); (2) abc.intReturningMethod(); result = 3; (3) abc.stringReturningMethod(); returns("str1", "str2"); result = new SomeCheckedException(); } }; new UnitUnderTest().doSomething(); }
这里记录了三种不同的期望值。第一个(其实就是 DependencyAbc() 的构造函数调用)实际上会在测试代码中通过一个无参的构造函数来初始化这些依赖,对于这种调用是不需要任何返回值的,除非在构造函数里面抛出一个异常或者 错误(其实构造函数是没有返回值的,所以对它来说记录返回值是没什么意义可说)。第二个期望指定调用intReturningMethod()后将返回值 3。第三个期望就是,调用stringReturningMethod()方法后将按顺序返回3个连续的期望值,注意下,最后一个结果其实是一个需要检查 的异常实例,这样允许测试代码去到达它最初的目的(注意:只有异常没有被传播出去(propagated out),它才会被传递。《这里翻译得怪怪,后面再看吧》)
对于一个返回值不是void的mock方法,无论是否匹配上在Record阶段定义的调用期望,都应该对该方法提供默认的返回值。Jmockit总 会根据定义返回值的类型返回一个值:对于整型缺省返回0,boolean类型默认为false,collection或者array会默认为empty对 象,而对于引用类型,则默认为null(包括String类型和JDK原始的包装类)。同样,对于mocked的构造函数和返回值为void的 mocked方法,也提供一个"缺省值",只不过就是简单的return而已,当然没有抛出异常或者错误。(除非,在重播(Replay)阶段,程序发现 一些不符合期望的返回值,对于这种情况,jmockit会自动向外抛出AssertionError 错误,从而是单元测试不通过)。
当然,我们可以在一个期望块中通过声明 indirect input 域,来重写这些默认的缺省值。只是,这些( 实例)域必须使用@Input注解尽心声明。下面就是这样的一个例子。
@Test public void someTestMethod() { new NonStrictExpectations() { DependencyAbc mock; // The names of input fields are merely for documentation. @Input final int defaultIntReturn = 5; @Input Socket aSocket; @Input FileNotFoundException onFileOpen; { abc.stringReturningMethod(); returns("str1", "str2"); } }; new UnitUnderTest().doSomething(); }
在这些返回值当中,只是必须保证定义的input域的类型必须和真实的返回值类型是一致的。任何的一个方法,只要其返回值类型和input域的的类 型一样,那么将返回@input域的缺省值。这对于在测试代码中,每次的调用都是一样的,而且不需要在expectation 期望块中有任何记录与之匹配。在上面的例子中,类DependencyAbc 的所有返回值类型是int的方法都将返回5,任意返回类型是Socket的方法调用都自动调用 java.net.Socket的无参构造函数并返回(如果需要,我们可以显式的初始化一个实例,并赋值给当前域)。
对于那些声明了会对外抛出异常的方法和构造函数,同样可以拥有一个特定的缺省值,只是简单的通过声明一个input域就行。可以直接初始化一个异常 实例赋值给该input域。如果不这样做的话,jmockit会自动调用无参的exception构造函数进行初始化赋值给该域。在上面的例子中,如果类 DependencyAbc 有一个声明异常为va.io.FileNotFoundException的方法或者构造函数,如果在测试代码调用中产生了异常,系统将抛出该缺省的异常 类型,当然,其前提是这次调用没有匹配上任何的recorded调用。
注意到,这个机制是只是依赖于返回值类型和声明抛出的异常类型,而不是依赖于方法名字或者参数,也不依赖mocked的class或者实例调用。在 正式的生产环境的多数方法中,我们通常发现,当一个方法被调用,只需要一个固定类型的返回值而已(特别是当返回值类型是一个引用类型,或者是原生类型或者 String类型)。下面的例子(上面已经出现过了)是满足最初的它最初的需求,假设我们不是只希望在第三次循环中才catch异常。当然,如果确实是只 希望在第三次循环中才catch异常,那么
@Test public void doSomethingHandlesSomeCheckedException() { new Expectations() { DependencyAbc abc; (2) @Input int iterations = 3; (3) @Input SomeCheckedException onFirstIteration; }; new UnitUnderTest().doSomething(); }
The onInstance(m) constraint
我们可以在expectation块中,使用Expectations#onInstance(mockObject)这个方法来约束匹配,就好像下面所示一样。
@Test public void matchOnMockInstance(final Collaborator mock) { new Expectations() {{ onInstance(mock).getValue(); result = 12; }}; //执行测试方法参数传递下来的mock实例 int result = mock.getValue(); assertEquals(12, result); //如果在测试代码内部中创建另外一个实例... Collaborator another = new Collaborator(); // ...这里我们获取不到在Expectations期望块中记录的返回结果,而是得到一个缺省值0 assertEquals(0, another.getValue()); }
上面的测试代码只有在以下这种情况下才可以执行通过:在测试代码中调用了和记录阶段声明的调用实例的getValue()方法才行。对于那些存在两 个或者更多个具有相同类型但不同实例的情况,这通常来说是很有用的。同样,如果我们需要判断测试代码中每一个调用都发生在合适的对象实例时,这也是很有用 的。
在测试代码中如果需要使用同一类型的多个不同实例时,为了防止每一个expectation 期望都需要使用onInstance(m)方法,Jmockit在 mock集合范围中自动推断出来是否需要"onInstance" 进行匹配。特别是,当在测试代码中存在两个或者多个类型相同的 field域和 parameter参数时,实例的调用会自动匹配到expectation记录中的相同实例。因此,对于一般情况来说,没什么必要去显式使用 onInstance(m)方法。
假设我们需要测试这样的代码:这些代码执行时需要一个给定的类的多个实例。如果我们声明该类被mocked掉,那么这个类的所有实例都会变得一样: 他们都变成了mock实例了,因此任何实例的方法调用都会根据我们mock的实现来处理。然而,如果我们只需要这些实例其中某一个(或者某一些)才需要被 mock,而其他实例的调用都是执行最原始的真实实现,此时我们该怎样?这就是注解@Injectable 的用武之地了。(它同样有其他的用法,我们稍后会再讨论)。
通过使用注解@Injectable声明一个mock域或者mock参数,我们可以获取一个"独特"的mock实例,而其他同一个 mock类型的实例,会仍然保持原有的未被 mock实例,除非它和一个隔离的mock 域关联在一起。既然一个injectable mocked实例是用来让其他实例保持原始的实现,那么它的静态方法和构造函数同样不会被 mock(译者注:这是很自然的,如果这个特性都保持不了,则没法保证其他实例拥有原始的实现,因为静态方法和构造函数是各个实例共享的)。毕竟一个静态 方法并不是和任何一个类实例关联的,而构造函数只是用于来创建实例(只是各实例之间会有所不同而已)。
举一个例子,我们来测试下面的类。
public static final class ConcatenatingInputStream extends InputStream { private final Queue<InputStream> sequentialInputs; private InputStream currentInput; public ConcatenatingInputStream(InputStream... sequentialInputs) { this.sequentialInputs = new LinkedList<InputStream>(Arrays.asList(sequentialInputs)); currentInput = this.sequentialInputs.poll(); } @Override public int read() throws IOException { if (currentInput == null) return -1; int nextByte = currentInput.read(); if (nextByte >= 0) { return nextByte; } currentInput = sequentialInputs.poll(); return read(); } }
使用ByteArrayInputStream 对象作为输入参数,我们可以很容易测试上面的类,即使不适用任何mock技术。但是我们需要确保的是,InputStream#read() 方法被正确的调用在每一个 input stream输入流(这些输入流是通过构造函数来传递的)上。下面的测试代码会达到这个目的。
@Test public void concatenateInputStreams( @Injectable final InputStream input1, @Injectable final InputStream input2) throws Exception { new Expectations() {{ input1.read(); returns(1, 2, -1); input2.read(); returns(3, -1); }}; InputStream concatenatedInput = new ConcatenatingInputStream(input1, input2); byte[] buf = new byte[3]; concatenatedInput.read(buf); assertArrayEquals(new byte[] {1, 2, 3}, buf); }
请注意,在这里使用@Injectable 注解是很有必要的。因为这个测试类已经扩展了mocked的 class(译者注:其实就是InputStream ),后面定义了需要执行的方法。如果InputStream 按照正常的方式来mock , 那么 read(byte[]) 方法会一直被 mock,无论这个方法是在哪个实例上被调用。(好吧,我们还是可以编写一个不使用injectable 实例的测试代码,只要我们在测试代码中重写read()方法)。
给定一个测试用例,我们通常是不知道这些参数值到底是什么,或者这些参数对于测试的单元并不是必须的。所以,我们可以通过指定一个具有伸缩性(或者 说是灵活吧)参数匹配约束,而不是使用精确的参数值匹配约束,从而允许测试代码在重播阶段通过不同的参数值也可以匹配Record或者Verified阶 段声明的 期望调用集合。这个功能是通过使用withXyz(...)方法和 (或者)anyXyz域来实现。这些带有前缀"with"的方法 和前缀"any"的域都是定义在基类 mockit.Invocations里面。这个基类是mockit.Expectations和 mockit.Verifications的父类。因此,这些方法和域可以同时在Expectations和Verifications块中使用。
@Test public void someTestMethod(@NonStrict final DependencyAbc abc) { final DataItem item = new DataItem(...); new Expectations() {{ // 那些第一个参数等于"str"而且第二个参数不为null的调用将匹配"voidMethod(String, List)"方法. abc.voidMethod("str", (List<?>) withNotNull()); //对于类 DependencyAbc的实例,如果调用的stringReturningMethod(DataItem, String)的方法, //满足第一个参数指针指向同一个"item",而且第二个参数俺有字符串 "xyz".那么该次调用将匹配下面的期望 abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz")); }}; new UnitUnderTest().doSomething(item); new Verifications() {{ // 匹配所有参数为任何long类型的方法调用. abc.anotherVoidMethod(withAny(1L)); }}; }
在Jmockit的世界里,存在更多"with"类型的方法,而不仅仅只是上面例子所示的那些。更详细可以参考API文档 。
除了几个预定义的参数匹配的约束API,JMockit允许用户通过<T> T with(Object) and <T> T with(T, Object>) 这样的泛型方法来提供自定义的约束。参数Object类型可以是org.hamcrest.Matcher 对象(Hamcrest 库的对象),或者是一个合适的句柄对象(更多细节可以参考API文档 )
最常见的参数匹配约束往往就是限制条件最少的那种匹配约束:匹配任何一个给定的参数(当然是正确的参数类型)的值调用。为此,我们有一个参数匹配的 特殊的属性字段集合,一些是为匹配所有的原始类型(包括相应的包装类),一些是用于字符串,以及一些用于匹配"通用"类型的对象。下面的测试演示了一些使 用。
@Test public void someTestMethod(@NonStrict final DependencyAbc abc) { final DataItem item = new DataItem(...); new Expectations() {{ // 这里期望的声明,会匹配所有这样的"voidMethod(String, List)"方法调用: // 其第一个参数为任意字符串,而且第二个参数是任何一个list实例对象 abc.voidMethod(anyString, (List<?>) any); }}; new UnitUnderTest().doSomething(item); new Verifications() {{ // 匹配参数类型是long或者Long的方法调用 abc.anotherVoidMethod(anyLong); }}; }
使用"with"方法的调用语句的规定适用于使用"any"字段的使用情况:他们必须出现在方法调用的实际参数位置上,而不是在此之前。对于参数匹配的属性字段更多详细内容可以参考API文档 。
对于给定的一个期望,当我们需要使用至少一个参数匹配方法或者字段时,我们可以使用一个 "便捷"的方式去指定该期望接受所有任意的对象引用(引用类型的参数),只需使用一个null值,而不是使用withAny(X)或"any'属性字段, 特别是,这样可以不需要将值的类型转换为参数声明时的类型。然而,需要记住的是,这种行为只适合这样的期望,它使用了至少一个显式的参数匹配(或 是"with"方法,或是"any"属性字段)。当null值传递给一个没有任何匹配的调用时,空值将只匹配空引用。在前面的测试,因此我们可以这样写:
@Test public void someTestMethod(@NonStrict final DependencyAbc abc) { ... new Expectations() {{ abc.voidMethod(anyString, null); }}; ... }
当一个或者多个参数匹配同时使用时,而且对于给定的参数必须匹配null引用,那么withNull()就应该被使用。
总之,这里有两个参数匹配模式:一个基本的匹配是,没有任何的匹配约束指定所有参数必须是相等的;而另一个匹配是,存在一个匹配指定部分或全部参数 对应一个匹配的约束。但null值和上面的每个模式都不太一样,这可能会导致混乱。不过,对于涉及多个参数的复杂调用,可以使用"any"属性字段和 null引用的好处大于附加在API上的复杂性。
有时,我们可能需要处理带有"可变参数"的方法或构造函数的期望。通过传递一个常规值来作为一个可变参数值是有效的,同样,使 用"with"、"any"匹配器来匹配也是有效的。然而,对于一个结合了两种值传递的相同期望,这并不是有效的。我们要么只能使用常规值或者参数匹配 值。
这种情况下,我们要匹配可变参数接收任何值(包括零)的调用,对这样的可变参数,我们可以指定一个期望使用个(Object[])any的约束来进行匹配。
也许最好的方式来理解可变参数匹配的确切语义(因为没有涉及特定的API)是阅读或实践实际的测试代码。这个测试类 演示了几乎所有的可能性。
到目前为止,我们可以看出,一个expectation除了可以关联一个方法或者一个构造函数,还是可以指定调用的返回结果和参数匹配约束。在下面 这种情况下:在单元测试代码中,需要多次调用同一个方法或者构造函数,但其参数是不同的,此时,我们需要一种方法去声明期望去满足这些相互独立的调用。一 种方式是,就好像之前所见的,就是简单的为每一个调用声明一个独立的期望,声明顺序保持和调用执行时的顺序。另一种方式,对单个expectation期 望声明记录下两个或者更多个连续的返回结果。
而然,还存在另外一种方式,就是对一个给定的期望,指定该期望对应的调用执行次数的约束。为此,jmockit提供了三个特定的属性字段 域:times, minTimes和maxTimes。这些属性字段是属于mockit.Invocations类的,它是mockit.Expectations和 mockit.Verifications的一个非public的基类。因此,一个调用次数的约束可以用于记录阶段和检验阶段。在这几种情况下,与期望相 关联的方法或构造,在指定范围内将受到指定的调用次数的限制。一个调用如何少于期望的最少执行次数,或者超过期望的执行次数的上限,这时,单元测试会自动 失败。让我们看下面一些例子:
@Test public void someTestMethod(final DependencyAbc abc) { new Expectations() {{ // By default, one invocation is expected, i.e. "times = 1": new DependencyAbc(); // At least two invocations are expected: abc.voidMethod(); minTimes = 2; // 1 to 5 invocations are expected: abc.stringReturningMethod(); minTimes = 1; maxTimes = 5; }}; new UnitUnderTest().doSomething(); } @Test public void someOtherTestMethod(final DependencyAbc abc) { new UnitUnderTest().doSomething(); new Verifications() {{ // Verifies that zero or one invocations occurred, with the specified argument value: abc.anotherVoidMethod(3); maxTimes = 1; // Verifies the occurrence of at least one invocation with the specified arguments: DependencyAbc.someStaticMethod("test", false); // "minTimes = 1" is implied }}; }
但不同于result属性字段的是,对于一个给定的期望,这三个属性字段最多只可以被指定一次。对于任何的调用次数的约束,一个非负整数都是有效 的。如果times=0或者maxTimes=0,那么在重播阶段(如果存在),发现存在一个调用能匹配上期望,则测试用例会因此失败。了解更多,请参见API文档 。