下图为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 }}
在我们了解记录,重放和验证调用之间的差异后,我们将更多地了解期望。
record-replay-verify模型
任何开发人员测试都可以分为至少三个单独的执行阶段。 这些阶段按顺序执行,一次一个,如下所示。
1 @Test 2 public void someTestMethod(){ 3 //1.准备:在被测试代码可以执行之前的任何需要。 4 ... ... 5 // 2.通过调用public方法来执行测试下的代码。 6 ... ... 7 // 3.验证:无论需要检查以确保代码行使 8 // 测试完成了它的工作。 9 ... ... 10 }}
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 }}
上述模板还有其他变化,但本质是,期望块属于记录阶段,并且在被测试代码执行之前,而验证块属于验证阶段。 测试方法可以包含任何数量的期望块,也可以没有。 验证块也是如此。事实上,匿名内部类用于区分代码块,这使我们可以利用现代Java IDE中提供的“代码折叠”功能。 下图显示了IntelliJ IDEA中的内容。
定期与严格的期望
严格和非严格的模拟
注意,我们不指定给定的模拟类型/实例应该是严格的或不。相反,给定模拟场/参数的严格性由如何在测试中使用来确定。一旦在“新的StrictExpectations(){...}”块中记录了第一个严格的期望,相关的模拟类型/实例被认为对于整个测试是严格的;否则,就不会严格。
记录期望的结果
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 }}
匹配调用特定实例(@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 返回顶部
声明多个mock实例
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 }}
使用给定构造函数创建的实例
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 }}
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 }}
参数值的灵活匹配
使用“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 }}
使用“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 }}
使用空值匹配任何对象引用
1 @Test 2 public void someTestMethod(@Mocked final DependencyAbc abc){ 3 ... ... 4 new Expectations(){{ 5 abc.voidMethod(anyString,null); 6 }}; 7 ... ... 8 }}
通过varargs参数传递的匹配值
指定调用计数约束
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 }}
显示验证
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 }}
验证调用从未发生
验证按顺序
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 }}