下图为jmockit 类图。在我们编写代码时几乎都会用到Expectations(期望)和Verifications(校验),二者均继承自Invacations.
常会用到的注解有:@Mocked @Tested @Injectable(@Tested和@Injectable经常配对使用),@Capturing(用于接口)
mock类型和实例
从依赖的测试代码调用的方法和构造函数是mock(模拟)的目标。 Mocking提供了我们需要的机制,以便将被测试的代码与(一些)依赖关系隔离开来。我们通过声明适当的模拟字段和/或模拟参数来指定要为给定测试(或多个测试)模拟哪些特定依赖性; mock字段声明为测试类的注释实例字段,而mock参数声明为测试方法的注释参数。 要模拟的依赖关系的类型将是模拟字段或参数的类型。这种类型可以是任何类型的引用类型:接口,类(包括抽象和final类型),注释或枚举。
默认情况下,mock类型的所有非私有方法(包括静态,final或native的任何方法)将在测试期间被模拟。如果声明的mocked类型是一个类,那么所有的超类直到但不包括java.lang.Object也将被递归地mock。同时,继承的方法也会自动被mock。在一个类的情况下,它的所有非私有构造函数将被mock。
当一个方法或构造器被mock时,它的原始实现代码将不会被执行在测试期间发生的调用。相反,调用将被重定向到JMockit,所以它可以以显式或隐式指定测试方式处理。
以下示例测试框架用作对mock字段和mock参数的声明以及它们在测试代码中通常使用的方式的基本说明。
对于在测试方法中声明的mock参数,声明类型的实例将由JMockit自动创建,并在JUnit / TestNG测试运行器执行测试方法时传递; 因此,参数值永远不会为null。对于mock属性,声明类型的实例将由JMockit自动创建并分配给字段,前提是它不是final(final不允许继承)。
有一些不同的注解可用于声明模拟属性和参数,以及默认模拟行为可以修改以适应特定测试的需要的方式。本章的其他部分详细介绍,但基本是:@Mocked是中央模拟注解,有一个可选的属性,在某些情况下是有用的; @Injectable是另一个模拟注释,它限制mock单个模拟实例的实例方法;@Capturing是另一个模拟注释,它扩展mock到实现模拟接口的类,或扩展mock类的子类。当@Injectable或@Capturing应用于模拟字段或模拟参数时,隐含了@Mocked,所以它不需要(但可以)应用。由JMockit创建的模拟实例可以在测试代码(用于期望的记录和验证)中正常使用,和/或传递到测试中的代码。或者他们可能只是闲置。与其他模拟API不同,这些模拟对象不一定是被测试代码在其依赖项上调用实例方法时使用的对象。默认情况下(即,当不使用@Injectable时),JMockit不关心调用模拟实例方法的对象。这允许直接在测试下的代码中创建的实例的透明模拟,当所述代码使用新运算符调用全新实例上的构造函数时;实例化的类必须由测试代码中声明的模拟类型覆盖,这就是全部。
Expectations
期望表示对与给定测试相关的特定模拟方法/构造函数的调用的集合。期望可以覆盖对同一方法或构造器的多个不同调用,但是它不必覆盖在测试的执行期间发生的所有这样的调用。特定调用是否与给定期望匹配将不仅取决于方法/构造函数签名,而且还取决于诸如调用该方法的实例,参数值和/或已经匹配的调用的数量的运行时方面。因此,可以(可选地)为给定期望指定几种类型的匹配约束。
当我们具有一个或多个调用参数时,可以为每个参数指定确切的参数值。例如,可以为String参数指定值“test string”,从而导致期望仅在相应参数中将这些调用与此精确值进行匹配。正如我们将在后面看到的,我们可以指定更宽松的约束,而不是指定精确的参数值,它将匹配整组不同的参数值。
下面的示例显示了Dependency#someMethod(int,String)的期望,它将使用指定的确切参数值匹配此方法的调用。注意,期望本身是通过对模拟方法的单独调用来指定的。没有涉及特殊的API方法,这在其他mocking API中是常见的。然而,这种调用不算作我们对测试感兴趣的“真正的”调用之一。它只在那里,以便可以指定期望。
1 @Test
2 public void doBusinessOperationXyz(@Mocked final Dependency mockInstanc){
3 ... ...
4 new Expectations(){{
5 ... ...
6 //实例方法的期望:
7 //mockInstance.someMethod(1,“test”);
8 result =“mocked”;
9 ... ...
10 }};
11 //这里调用被测代码,导致模拟调用可能或可能不匹配指定的期望。
12 }}
View Code
在我们了解记录,重放和验证调用之间的差异后,我们将更多地了解期望。
record-replay-verify模型
任何开发人员测试都可以分为至少三个单独的执行阶段。 这些阶段按顺序执行,一次一个,如下所示。
1 @Test
2 public void someTestMethod(){
3 //1.准备:在被测试代码可以执行之前的任何需要。
4 ... ...
5 // 2.通过调用public方法来执行测试下的代码。
6 ... ...
7 // 3.验证:无论需要检查以确保代码行使
8 // 测试完成了它的工作。
9 ... ...
10 }}
View Code
首先,我们有一个准备阶段,其中测试所需的对象和数据项从其他地方创建或获取。然后,执行测试中的代码。最后,将运行测试代码的结果与预期结果进行比较。
这个三阶段模型也称为Arrange,Act,Assert语法,或简称为“AAA”。不同的话,但意思是一样的。
在使用模拟类型(及其模拟实例)的基于行为的测试的上下文中,我们可以识别以下替代阶段,这与前面描述的三个常规测试阶段直接相关:
记录阶段,在此阶段可以记录调用。这发生在测试准备期间,在我们要测试的调用被执行之前。
在重放阶段期间,当执行测试代码时,感兴趣的模拟调用有机会被执行。之前记录的对模拟方法/构造函数的调用现在将被重放。通常在记录和重放的调用之间没有一对一的映射。
验证阶段,在此期间,可以验证调用已按预期发生。这发生在测试验证期间,在测试下的调用有机会被执行之后。
使用JMockit编写的基于行为的测试通常适合以下模板:
1 import mockit。*;
2 ...other import...
3 public class SomeTest{
4 //零或多个“模拟字段”对类中的所有测试方法是通用的:
5 @Mocked Collaborator mockCollaborator;
6 @Mocked AnotherDependency anotherDependency;
7 ... ...
8 // 第一种情况:
9 @Test
10 public void testWithRecordAndReplayOnly(mock parameters) {
11 //准备代码不特定于JMockit,可以使任意的。
12 new Expectations(){{
13 //“expectation block”
14 //对模拟类型的一个或多个调用,导致期望被记录。
15 //在这个块内的任何地方也允许调用非模拟类型
16 //(虽然不推荐)。
17 }};
18 //执行被测单元。
19 //验证代码码(JUnit/TestNG assertions),如果有的话。
20 }}
21 // 第二种情况:
22 @Test
23 public void testWithReplayAndVerifyOnly(mock parameters) {
24 准备代码不特定于JMockit,可以使任意的。
25 执行被测单元。
26 new Verifications() {{
27 //“验证块”
28 //对模拟类型的一个或多个调用,导致期望被验证。
29 //在这个块内的任何地方也允许调用非模拟类型
30 //(虽然不推荐)。
31 }};
32 //附加验证码(如果有的话),位于验证区块之前或之前。
33 }}
34 // 第三种情况:
35 @Test
36 public void testWithBothRecordAndVerify(mock parameters) {
37 //准备代码不特定于JMockit,如果有的话。
38 //new Expectations(){{
39 ////对模拟类型的一个或多个调用,导致期望被记录。
40 }};
41 //执行被测单元。
42 new VerificationsInOrder(){{
43 //有序的验证块
44 //对模拟类型的一个或多个调用,导致期望被验证
45 //以指定的顺序。
46 }};
47 //附加验证码(如果有的话),位于验证区块之前或之前。
48 }}
49 }}
View Code
上述模板还有其他变化,但本质是,期望块属于记录阶段,并且在被测试代码执行之前,而验证块属于验证阶段。 测试方法可以包含任何数量的期望块,也可以没有。 验证块也是如此。事实上,匿名内部类用于区分代码块,这使我们可以利用现代Java IDE中提供的“代码折叠”功能。 下图显示了IntelliJ IDEA中的内容。
定期与严格的期望
“new Expectations(){...}”块中记录的期望是常规期望。这意味着期在重放它们指定的调用预阶段期间至少发生一次;它们可能出现不止一次,但是相对于其他记录的期望以不同的顺序出现;另外,允许不以任何记录的期望匹配的调用以任何数量和以任何顺序发生。如果没有调用匹配给定的记录期望,则在测试结束时抛出“丢失的调用”错误,导致它失败(这只是默认行为,因为它可以被覆盖)。
API还支持严格期望的概念:在记录时,仅允许在重放期间完全匹配记录(在明确指定的允许(在需要时)中)的调用,在匹配调用的数量(默认为一个)和它们发生的顺序。在重放期间发生但无法匹配记录的严格期望的调用被视为意外的,导致立即的“意外调用”错误,因此测试失败。这是通过使用StrictExpectations子类实现的。
注意,在严格期望的情况下,在重放期间发生的匹配记录的期望的所有调用被隐式地验证。任何与预期不匹配的剩余调用都被视为意外,导致测试失败。如果错过任何记录的严格期望,即如果在重放期间没有发生匹配的调用,则测试也将失败。
我们可以通过编写多个期望块,一些规则(使用期望),其他严格(使用StrictExpectations)在同一测试中混合不同严格程度的期望。通常,给定的模拟字段或模拟参数将出现在单一类型的期望块中。
大多数测试将简单地利用“常规”期望。使用严格的期望可能更多是个人喜好的问题。
严格和非严格的模拟
注意,我们不指定给定的模拟类型/实例应该是严格的或不。相反,给定模拟场/参数的严格性由如何在测试中使用来确定。一旦在“新的StrictExpectations(){...}”块中记录了第一个严格的期望,相关的模拟类型/实例被认为对于整个测试是严格的;否则,就不会严格。
记录期望的结果
对于具有非void返回类型的给定方法,可以通过对结果字段赋值来记录返回值。当在重放阶段调用该方法时,指定的返回值将返回给调用者。对结果的赋值应该出现在调用之后,该调用标识期望块内的记录期望。
如果测试改为需要在调用方法时抛出异常或错误,那么仍然可以使用结果字段:只需为其分配所需的可抛出的实例。注意,要抛出的异常/错误的记录适用于模拟方法(任何返回类型)以及模拟构造函数。
通过在一行中多次分配结果字段,可以针对相同的期望记录多个连续结果(要返回的值和/或可抛出的throwable)。对于相同的期望,可以自由地混合要抛出的多个返回值和/或异常/错误的记录。在为给定期望记录多个连续返回值的情况下,可以对返回(Object ...)方法进行单个调用。此外,如果为其分配的值是包含连续值的列表或数组,则对结果字段的单个分配将实现相同的效果。
以下示例测试记录了模拟的DependencyAbc类的方法的两种类型的结果,当从UnitUnderTest类调用时,将使用它们。让我们说测试类的实现如下:
1 public class UnitUnderTest{
2 (1)private final DependencyAbc abc = new DependencyAbc();
3 public void doSomething(){
4 (2)int n = abc.intReturningMethod();
5 for(int i = 0; i <n; i ++){
6 String s;
7 catch{
8 (3)s = abc.stringReturningMethod();
9 }}
10 catch(SomeCheckedException e){
11 //以某种方式处理异常
12 }}
13 //做一些其他的东西
14 }}
15 }}
16 }}
17 // doSomething()方法的一个可能的测试可以运行在任意数量的成功迭代之后抛出SomeCheckedException的情况。假设我们想要(为了什么原因)记录这两个类之间的交互的一整套期望,我们可以写下面的测试。 (通常,指定对模拟方法的所有调用和特定的在给定测试中的模拟构造函数是不可取的或重要的,我们稍后将解决这个问题。
18 @Test
19 public void doSomethingHandlesSomeCheckedException(@Mocked final DependencyAbc abc)throws Exception{
20 new Expectations(){{
21 (1)new DependencyAbc();
22 (2)abc.intReturningMethod();
23 result = 3;
24 (3)abc.stringReturningMethod();
25 returns(“str1”,“str2”);
26 result = new SomeCheckedException();
27 }};
28 new UnitUnderTest().doSomething();
29 }}
View Code
该测试记录了三种不同的期望。第一个,由对DependencyAbc()构造函数的调用表示,仅仅说明这个依赖关系恰好在被测代码中通过no-args构造函数实例化的事实;除非偶尔抛出的异常/错误(构造函数具有void返回类型,因此对它们记录返回值没有意义),因此不需要为这样的调用指定结果。第二个期望指定当调用时intReturningMethod()将返回3。第三个字符串指定了stringReturningMethod()的三个连续结果的序列,其中最后一个结果恰好是所需异常的一个实例,允许测试实现其目标(注意,如果异常没有传播出去,它才会通过)。
匹配调用特定实例(@Injectable)
上面,我们解释了在模拟实例上记录的期望,例如“abc.someMethod();”实际上匹配调用DependencyAbc#someMethod()在模拟的DependencyAbc类的任何实例上。在大多数情况下,测试代码使用给定依赖关系的单个实例,因此这不会真正重要,可以安全地忽略,无论模拟的实例是传递到测试中的代码还是在其中创建。但是,如果我们需要验证调用发生在特定实例上,如果在测试中的代码中使用几个调用之间呢?此外,如果只有一个或几个模拟类的实例实际上应该被模拟,同一个类的其他实例保持未被捕获怎么办? (这种第二种情况往往发生在来自标准Java库或其他第三方库的类被模拟时)。API提供了一个模拟注释@Injectable,它只会模拟mock类型的一个实例,使其他人不受影响。此外,我们有几种方法来约束期望与特定@Mocked实例的匹配,同时仍然mock所有模拟类的实例。
可注入的模拟实例
假设我们需要测试与给定类的多个实例一起工作的代码,其中一些我们想要模拟。如果要被模拟的实例可以传递或注入到测试中的代码,那么我们可以为它声明一个@Injectable模拟字段或模拟参数。这个@Injectable实例将是一个“独占”模拟实例;除非从单独的模拟字段/参数获得,否则相同模拟类型的任何其他实例将保持为常规的非模拟实例。
当使用@Injectable时,静态方法和构造函数也被排除在模拟之外。毕竟,静态方法不与类的任何实例相关联,而构造函数仅与新创建的(因此不同的)实例相关联。
例如,假设我们有下面的类来测试。
1 在线工具
2
3
4
5
6
7
8
9 保存(Save) 嵌入博客(Embed) 执行(Run)
10
11
12 1
13
14 public final class ConcatenatingInputStream extends InputStream{
15 2
16 private final Queue <InputStream> sequentialInputs;
17 3
18 private InputStream currentInput;
19 4
20
21 public ConcatenatingInputStream(InputStream ... sequentialInputs){
22 5
23 this.sequentialInputs = new LinkedList <InputStream>(Arrays.asList(sequentialInputs));
24 6
25 currentInput = this.sequentialInputs.poll();
26 7
27 }}
28 8
29 @Override
30 9
31
32 public int read()throws IOException{
33 10
34 if(currentInput == null)return -1;
35 11
36 int nextByte = currentInput.read();
37 12
38
39 if(nextByte> = 0){
40 13
41 return nextByte;
42 14
43 }}
44 15
45 currentInput = sequentialInputs.poll();
46 16
47 return read();
48 17
49 }}
50 18
51 }}
52 19
53 // 这个类可以通过使用ByteArrayInputStream对象进行输入而轻松地进行测试,但是我们希望确保InputStream#read()方法在构造函数中传递的每个输入流上被正确调用。以下测试将实现这一点。
54 20
55 @Test
56 21
57
58 public void concatenateInputStreams(@Injectable final InputStream input1,@Injectable final InputStream input2)throws Exception{
59 22
60
61 new Expectations(){{
62 23
63 input1.read();returns(1,2,-1);
64 24
65 input2.read();returns(3,-1); }};
66 25
67 InputStream concatenatedInput = new ConcatenatingInputStream(input1,input2);
68 26
69 byte [] buf = new byte [3];
70 27
71 concatenatedInput.read(buf);
72 28
73 assertArrayEquals(new byte [] {1,2,3},buf);
74 29
75 }}
76
77
78 缩进 减少缩进 注释 格式化
79
80 227310278
81 小子欠扁
82
83 返回顶部
View Code
注意,@Injectable的使用在这里确实是必要的,因为被测试的类扩展了模拟类,并且调用练习ConcatenatingInputStream的方法实际上是在基于InputStream类中定义的。如果InputStream被“正常”mock,则read(byte [])方法总是被模拟,而不管它被调用的实例。
声明多个mock实例
当在同一mock字段/参数上使用@Mocked或@Capturing(而不是@Injectable)时,我们仍然可以将重放调用与记录在特定模拟实例上的期望进行匹配。为此,我们简单地声明多个模拟字段或同一模拟类型的参数,如下例所示。
1 @Test
2 public void matchOnMockInstance(@Mocked final Collaborator mock,@Mocked Collaborator otherInstance){
3 new Expectations(){{
4 mock.getValue(); result = 12;
5 }};
6 //使用从测试传递的模拟实例练习测试中的代码:
7 int result = mock.getValue();
8 assertEquals(12,result);
9 //如果另一个实例在测试下的代码中创建...
10 //Collaborator another = new Collaborator();
11 // ...我们不会得到记录的结果,但默认的一个:
12 assertEquals(0,another.getValue());
13 }}
View Code
上面的测试只有在测试代码(为了简洁嵌入在测试方法本身中)在对记录调用进行完全相同的实例上调用getValue()时才会通过。当被测试代码调用同一类型的两个或多个不同实例,并且测试想要验证特定调用是否发生在预期实例上时,这通常很有用。
使用给定构造函数创建的实例
特别是对于以后被测试的代码创建的未来实例,JMockit提供了一些机制,通过它们我们可以匹配对它们的调用。这两种机制都需要记录对模拟类的特定构造函数调用(“新”表达式)的期望。
第一种机制涉及在记录对实例方法的期望时简单地使用从记录的构造函数期望获得的新实例。让我们看一个例子。
1 @Test
2 public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator anyCollaborator){
3 //记录每组实例的不同行为:
4 new Expectations(){{
5 //一组,使用“a value”创建的实例:
6 Collaborator col1 = new Collaborator(“a value”);
7 col1.doSomething(anyInt); result = 123;
8 //另一个集合,使用“另一个值”创建的实例:
9 Collaborator col2 = new Collaborator("another value");
10 col2.doSomething(anyInt); result = new InvalidStateException();
11 }};
12 //测试代码:
13 new Collaborator(“a value”).doSomething(5);
14 //将返回123
15 ... ...
16 new Collaborator(“another value”).doSomething(0);
17 //会抛出异常
18 ... ...
19 }}
View Code
在上面的测试中,我们使用@Mocked声明所需类的单个模拟字段或模拟参数。然而,这个模拟场/参数在记录期望时不使用;相反,我们使用在实例化记录上创建的实例来记录对实例方法的进一步期望。使用匹配构造函数调用创建的未来实例将映射到这些记录的实例。另外,请注意,它不一定是一对一映射,而是一个多对一映射,从潜在的许多未来实例到用于记录期望的单个实例。
第二种机制允许我们记录与记录的构造函数调用匹配的那些未来实例的替换实例。有了这个替代机制,我们可以重写测试如下。
1 @Test
2 public void newCollaboratorsWithDifferentBehaviors(@Mocked final Collaborator col1, @Mocked final Collaborator col2){
3 new Expectations(){{
4 将单独的未来实例集映射到单独的模拟参数:
5 new Collaborator("a value"); result = col1;
6 new Collaborator("another value"); result = col2;
7 //记录每组实例的不同行为:
8 col1.doSomething(anyInt); result = 123;
9 col2.doSomething(anyInt); result = new InvalidStateException();
10 }};
11 //测试代码:
12 new Collaborator("a value").doSomething(5);
13 //将返回123
14 ... ...
15 new Collaborator("another value").doSomething(0);
16 //会抛出异常
17 ... ...
18 }}
View Code
这两个版本的测试是等效的。第二个也允许,当与部分模拟组合时,用于实际(非模拟)实例用作替换。
参数值的灵活匹配
在记录和验证阶段,对模拟方法或构造函数的调用标识期望。如果方法/构造函数有一个或多个参数,那么记录/验证期望如doSomething(1,“s”,true);将只匹配重放阶段中的调用(如果它具有相等的参数值)。对于作为常规对象(非基元或数组)的参数,equals(Object)方法用于相等性检查。对于数组类型的参数,等式检查扩展到单个元素;因此,在每个维度中具有相同长度的两个不同的阵列实例和等同的对应元素被认为是相等的。
在给定的测试中,我们经常不知道这些参数值将是什么,或者它们对于被测试的东西不是必需的。因此,为了允许记录或验证的调用来匹配具有不同参数值的整个重放调用的集合,我们可以指定灵活的参数匹配约束而不是实际的参数值。这是通过使用anyXyz字段和/或withXyz(...)方法。 “任何”字段和“with”方法都在mockit.Invocations中定义,它是测试中使用的所有期望/验证类的基类;因此,它们可以用于期望以及验证块。
使用“any”字段进行参数匹配
最常见的参数匹配约束也倾向于是最少限制的:匹配调用与给定参数(当然,适当的参数类型)的任何值。对于这种情况,我们有一整套特殊的参数匹配字段,一个用于每个基本类型(和相应的包装器类),一个用于字符串,一个“通用”类型的对象。下面的测试展示了一些用途。
1 @Test
2 public void someTestMethod(@Mocked final DependencyAbc abc){
3 final DataItem item = new DataItem(...);
4 new Expectations(){{
5 将匹配第一个参数为“voidMethod(String,List)”调用
6 //任何字符串和第二个任何列表。
7 abc.voidMethod(anyString,(List <?>)any);
8 }};
9 new UnitUnderTest().doSomething(item);
10 new Verifications() {{
11 //匹配具有long或Long类型的任何值的指定方法的调用。
12 abc.anotherVoidMethod(anyLong);
13 }};
14 }}
View Code
“any”字段的使用必须出现在调用语句中的实际参数位置,从不在之前。你仍然可以在同一调用中的其他参数的常规参数值。
使用“with”方法进行参数匹配
当记录或验证期望时,对调用中传递的参数的任何子集可以调用withXyz(...)方法。它们可以与常规参数传递(使用文字值,局部变量等)自由混合。唯一的要求是这样的调用出现在记录/验证的调用语句内,而不是在它之前。例如,不可能首先将调用的结果分配给局部变量withNotEqual(val),然后在调用语句中使用该变量。使用一些“with”方法的示例测试如下所示。
1 @Test
2 public void someTestMethod(@Mocked final DependencyAbc abc){
3 final DataItem item = new DataItem(...);
4 new Expectations(){{
5 将匹配“voidMethod(String,List)”调用第一个参数
6 //等于“str”,第二个不为null。
7 abc.voidMethod(“str”,(List <?>)withNotNull());
8 //将匹配调用到DependencyAbc#stringReturningMethod(DataItem,String)
9 //第一个参数指向“item”,第二个参数指向“xyz”。
10 abc.stringReturningMethod(withSameInstance(item),withSubstring(“xyz”));
11 }};
12 new UnitUnderTest().doSomething(item);
13 new Verifications() {{
14 //使用任何长整数参数匹配指定方法的调用。
15 abc.anotherVoidMethod(withAny(1L));
16 }};
17 }}
View Code
还有更多的“与”方法比上面所示。有关更多详细信息,请参阅API文档。
除了在API中可用的几个预定义参数匹配约束之外,JMockit允许用户通过with(Delegate)和withArgThat(Matcher)方法提供自定义约束。
使用空值匹配任何对象引用
当对给定期望使用至少一个参数匹配方法或字段时,我们可以使用“快捷方式”来指定应接受任何对象引用(对于引用类型的参数)。简单地传递null值,而不是一个withAny(x)或任何参数匹配器。特别是,这避免了将值转换为声明的参数类型的需要。但是,请记住,此行为仅适用于使用至少一个显式参数匹配器(“with”方法或“任何”字段)。当在不使用匹配器的调用中传递时,null值将仅匹配null引用。在之前的测试中,我们可以写成:
1 @Test
2 public void someTestMethod(@Mocked final DependencyAbc abc){
3 ... ...
4 new Expectations(){{
5 abc.voidMethod(anyString,null);
6 }};
7 ... ...
8 }}
View Code
要具体验证给定参数接收到空引用,可以使用withNull()匹配器。
通过varargs参数传递的匹配值
偶尔,我们可能需要处理“varargs”方法或构造函数的期望。它有效的传递常规值作为varargs参数,也有效使用“with”/“任何”匹配器的这样的值。但是,当有一个varargs参数时,对于相同的期望结合两种类型的值传递是无效的。我们需要仅使用常规值或仅使用通过参数匹配器获取的值。
如果我们想要匹配调用,其中varargs参数接收任意数量的值(包括零),我们可以为最终varargs参数指定一个期望值为“(Object [])any”约束。
指定调用计数约束
到目前为止,我们看到除了相关联的方法或构造函数之外,期望可以具有调用结果和参数匹配器。假设被测试的代码可以用不同或相同的参数多次调用相同的方法或构造函数,我们有时需要一种方法来解释所有这些单独的调用。
可以通过调用计数约束来指定期望和/或允许匹配给定期望的调用的数量。mockAPI只提供三个特殊字段:times,minTimes和maxTimes。这些字段可以在记录时或在验证期望时使用。在任一情况下,与期望相关联的方法或构造器将被约束以接收落入指定范围内的多个调用。任何调用分别小于或大于预期的下限或上限,并且测试执行将自动失败。让我们看看一些示例测试。
1 @Test
2 public void someTestMethod(@Mocked final DependencyAbc abc){
3 new Expectations(){{
4 默认情况下,至少需要一次调用,即“minTimes = 1”:
5 new DependencyAbc();
6 //至少需要两次调用:
7 abc.voidMethod(); minTimes = 2;
8 //需要1到5次调用:
9 abc.stringReturningMethod();
10 minTimes = 1;
11 maxTimes = 5;
12 }};
13 new UnitUnderTest().doSomething();
14 }}
15 @Test
16 public void someOtherTestMethod(@Mocked final DependencyAbc abc){
17 new UnitUnderTest().doSomething();
18 new Verifications() {{
19 验证发生了零个或一个调用,并指定了参数值:
20 abc.anotherVoidMethod(3);
21 maxTimes = 1;
22 //使用指定的参数验证至少一次调用的发生:
23 DependencyAbc.someStaticMethod(“test”,false);
24 //“minTimes = 1”
25 }};
26 }}
View Code
与结果字段不同,对于给定的期望,这三个字段中的每一个最多可以指定一次。任何非负整数值对于任何调用计数约束都有效。如果指定times = 0或maxTimes = 0,则与重放期间发生的期望匹配的第一次调用(如果有)将导致测试失败。
显示验证
除了对记录的期望指定调用计数约束之外,我们还可以在调用被测代码之后在验证块中显式地验证匹配调用。这对于常规期望是有效的,但对于严格的期望是无效的,因为它们总是被隐式地验证;在显式验证块中重新验证它们没有意义。
在“new Verifications(){...}”块中,我们可以使用“new Expectations(){...}”块中可用的相同API,但用于记录返回值的方法和字段除外,抛出异常/错误。也就是说,我们可以自由地使用anyXyz字段,withXyz(...)参数匹配方法,以及时间,minTimes和maxTimes调用计数约束字段。下面是一个示例测试。
1 @Test
2 public void verifyInvocationsExplicitlyAtEndOfTest(@Mocked final Dependency mock){
3 //这里没有记录,虽然它可以。
4 // Inside tested code:
5 Dependency dependency = new Dependency();
6 dependency.doSomething(123, true, "abc-xyz");
7 //验证Dependency#doSomething(int,boolean,String)被调用至少一次,
8 //具有遵守指定约束的参数:
9 new Verifications(){{
10 mock.doSomething(anyInt,true,withPrefix(“abc”));
11 }};
12 }}
View Code
请注意,默认情况下,验证检查在重放期间至少发生了一个匹配调用。当我们需要验证调用的确切数量(包括1)时,必须指定times = n约束。
验证调用从未发生
要在验证块中执行此操作,请在调用后添加一个“times = 0”分配,该分配在重放阶段期间不会发生。如果发生一个或多个匹配调用,测试将失败。
验证按顺序
使用验证类创建的常规验证块是无序的。在重放阶段期间调用aMethod()和anotherMethod()的实际相对顺序未经验证,但只有每个方法至少执行一次。如果要验证调用的相对顺序,则必须使用“new VerificationsInOrder() {...}" 块。在这个块中,只需按照它们预期发生的顺序向一个或多个模拟类型写入调用。
1 @Test
2 public void validationExpectationsInOrder(@Mocked final DependencyAbc abc){
3 //里面的测试代码:
4 abc.aMethod();
5 abc.doSomething(“blah”,123);
6 abc.anotherMethod(5);
7 ... ...
8 new VerificationsInOrder(){{
9 //这些调用的顺序必须与顺序相同
10 //重放匹配调用期间的发生。
11 abc.aMethod();
12 abc.anotherMethod(anyInt);
13 }};
14 }}
View Code
注意调用abc.doSomething(...)在测试中没有验证,所以它可能在任何时候发生(或根本不发生)。
部分有序验证
假设您要验证特定方法(或构造函数)在其他调用之前/之后被调用,但您不关心这些其他调用发生的顺序。在有序的验证块中,这可以通过在适当的地方简单地调用unverifiedInvocations()方法来实现。以下测试演示它。