翻译并没有完全遵循原文,有个人的理解与提炼。 如果你觉得有什么地方生涩难懂,欢迎反馈。谢谢!
更新时间:2017-05-23 23:16:35
Mocked types and instances mocked 的类和实例
方法包括构造方法是 mock 的目标。Mocking 提供了把被测代码和它的依赖隔离的机制。我们可以通过测试类的属性和测试方法的参数来声明 mock 对象。所有的引用类型都可以被 mock:interface, class (包括 final), annotation 和 enum 。
下面的代码演示了使用mock field 和参数的典型方式:
// "Dependency" is mocked for all tests in this test class. // The "mockInstance" field holds a mocked instance automatically created for use in each test. @Mocked Dependency mockInstance; @Test public void doBusinessOperationXyz(@Mocked final AnotherDependency anotherMock) { ... new Expectations() {{ // an "expectation block" ... // Record an expectation, with a given value to be returned: mockInstance.mockedMethod(...); result = 123; ... }}; ... // Call the code under test. ... new Verifications() {{ // a "verification block" // Verifies an expected invocation: anotherMock.save(any); times = 1; }}; ... }
上例中mock的参数是在JMockit生成之后,由Junit或TestNG的runner传入的。(显然,这就是为什么JMockit 依赖JUnit或TestNG 的原因)
有三种mock标注:
- @Mocked , mock 类及其基类。
- @Injectable , mock 类的某个实例,类的其它实例不受影响。
- @Capturing , mock 类及其衍生类。
注意这里提到的类也包括接口。
与其他的mock框架不同, @Mocked 和 @Capturing 缺省mock的是类。这意味着,一旦你这样" @Mocked SomeClass ins1" , 即使你调用构造方法new一个对象 ins2,ins2也是mock对象。
Expectations
一个expectation 是一个给定的测试要调用的方法包括构造方法的集合。它可能详细列出对同一个方法多次不同的调用,但这不是必须的。 在测试阶段,被测对象调用mock对象的方法的过程要与 expectation 匹配,但匹配的规则不仅仅限于方法的签名,还与运行时的因素有关系:对应的mock实例,参数值以及调用的次数等。Expactation 就是要指定这几个约束。
参数的匹配,我们可以精确匹配参数值,也可以松散的匹配。
与其他 mock 框架的不同,定制交互行为只需要在expectation 块内直接调用mock对象的方法,无需特殊的 API,但注意,这并非真的调用了那个方法,只是表达一个"期望" :
Test public void doBusinessOperationXyz(@Mocked final Dependency mockInstance) { ... new Expectations() {{ ... // An expectation for an instance method: mockInstance.someMethod(1, "test"); result = "mocked"; ... }}; // A call to code under test occurs here, leading to mock invocations // that may or may not match specified expectations. }
The record-replay-verify model 录制-重放-验证 模型
任何测试可以被划分到3各阶段:
@Test public void someTestMethod() { // 1. Preparation: whatever is required before the code under test can be exercised. ... // 2. The code under test is exercised, usually by calling a public method. ... // 3. Verification: whatever needs to be checked to make sure the code exercised by // the test did its job. ... }
这3个阶段也可以称为 Arrange, Act, Assert 。
在使用mock技术的基于行为的测试中,这三个阶段正好对应到:
- 录制 - 预期mock 对象可能被调用的方法, 及其应返回的结果;
- 回放 - 被测对象执行,调用 mock 对象的方法;
- 验证 - 检查mock 对象是否按照预期被调用;
当然,Expectations 块或 Verifacations 块可以有多个也可以没有。
Regular, strict, and non-strict expectations
Expectations : 指定的方法或次数必须满足,没指定的随意。但不关心顺序。
StrictExpectation : 意味着严格匹配。 replay 阶段调用了哪几个方法,调用的次数(没有指定就是1)和顺序,都必须和 record 阶段一样,否则测试fail (unexpected invocation)。
NonStrictExpectations : 无所谓,也就是相当于不做验证,只是用来指定result。建议仅被用于setup(@Before )阶段。
以上三者的区别意味着对于 StrictExpectation 不需要 Verifications 块,而NonStrictExpectations 需要。 ?终于明白它为啥叫 Expectations 了?
Recording results for an expectation
在Expectations 块内,对于 non-void 的方法,紧跟在方法调用之后给 result 赋值来指定返回值。
可以通过给 result 赋 Throwable 的实例来让方法抛出异常,这也适用于构造方法。
对与同一个方法连续调用而返回不同的值或异常,有3种选择可以办到:1,给result 赋值一个数组或list ; 2, 调用 results(Object ...) ; 3, 在同一行连续 对result 赋值多次。
看下面这个示例:
首先是被测的类:
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) { // somehow handle the exception } // do some other stuff } } }
其中 3 个数字是三个测试点,考虑要如何 expect 构造方法及对(3)的连续调用。
测试如下:
@Test public void doSomethingHandlesSomeCheckedException(@Mocked final DependencyAbc abc) throws Exception { new Expectations() {{ (1) new DependencyAbc(); (2) abc.intReturningMethod(); result = 3; (3) abc.stringReturningMethod(); returns("str1", "str2"); result = new SomeCheckedException(); }}; new UnitUnderTest().doSomething(); }
Matching invocations to specific instances
@Mocked 会作用于类的所有实例上。 但有时我们需要verify在指定实例上的方法调用,或者我们也同时需要mocked和实例。@Injectable 用于满足这个要求。
正因为@Injectable 只作用于指定的实例,所有不能mock构造方法和static的方法,也不影响基类。
示例:
被测类型:
public 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(); } }
测试:
@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 的原因是,我们不想影响基类 InputStream 的 read() 方法, 所以不能用 @Mocked。
The onInstance(m)
constraint
我们使用 @Mocked 或 @Capturing 时,也可以在record 阶段通过 onInstance(mocked) 方法来限制作用范围仅在指定的实例上。
@Test public void matchOnMockInstance(@Mocked final Collaborator mock) { new Expectations() {{ onInstance(mock).getValue(); result = 12; }}; // Exercise code under test with mocked instance passed from the test: int result = mock.getValue(); assertEquals(12, result); // If another instance is created inside code under test... Collaborator another = new Collaborator(); // ...we won't get the recorded result, but the default one: assertEquals(0, another.getValue()); }
实际上,如果 @Mocked 或 @Capturing 作用于相同类型的多个变量时,JMockit 会自动推断 record 是仅仅 match 指定的实例的。也就是说,不需要 onInstance() 方法。
Instances created with a given constructor
不同的构造方法产生的实例,record 不同的行为,可以采用以下两种方式之一:
方式一:
@Test public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator anyCollaborator) { // Record different behaviors for each set of instances: new Expectations() {{ // One set, instances created with "a value": Collaborator col1 = new Collaborator("a value"); col1.doSomething(anyInt); result = 123; // Another set, instances created with "another value": Collaborator col2 = new Collaborator("another value"); col2.doSomething(anyInt); result = new InvalidStateException(); }}; // Code under test: new Collaborator("a value").doSomething(5); // will return 123 ... new Collaborator("another value").doSomething(0); // will throw the exception ... }
注意这里,anyCollaborator 参数并没有用到,但这里 @Mocked 使得类型 Collaborator 成为Mocked 类型,方可对其方法和构造方法录制。
方式二:
@Test public void newCollaboratorsWithDifferentBehaviors( @Mocked final Collaborator col1, @Mocked final Collaborator col2) { new Expectations() {{ // Map separate sets of future instances to separate mock parameters: new Collaborator("a value"); result = col1; new Collaborator("another value"); result = col2; // Record different behaviors for each set of instances: col1.doSomething(anyInt); result = 123; col2.doSomething(anyInt); result = new InvalidStateException(); }}; // Code under test: new Collaborator("a value").doSomething(5); // will return 123 ... new Collaborator("another value").doSomething(0); // will throw the exception ... }
因为这里@Mocked 作用于两个同类型的变量,从而决定了之后对复发和的录制是关联到实例而非类。
Flexible matching of argument values 参数值的弹性匹配
在录制或者验证阶段, 不可能穷举所有可能的参数值,这时就需要使用弹性匹配。
弹性匹配通过两种方式表达,anyXxx 或 withXx() 方法,他们都是 Expectations 和 Verifacations 的基类 mockit.Invocations 的成员,所以对所有的Expectation 和 Verifaction 可用。
Using the "any" fields for argument matching
三类:原始及其包装类型,anyString , 还有一个 any 。
@Test public void someTestMethod(@Mocked final DependencyAbc abc) { final DataItem item = new DataItem(...); new Expectations() {{ // Will match "voidMethod(String, List)" invocations where the first argument is // any string and the second any list. abc.voidMethod(anyString, (List<?>) any); }}; new UnitUnderTest().doSomething(item); new Verifications() {{ // Matches invocations to the specified method with any value of type long or Long. abc.anotherVoidMethod(anyLong); }}; }
注意这里怎样表达 any list 的。
Using the "with" methods for argument matching
除了预定义的 withXx() :
实例: withSameInstance(), withEqual()/withNotEqual(), withNull()/withNotNull()
类型: withAny(T ) withInstanceLike() withInstanceOf()
String : withMatch(), withSubString(), withPrefix()/withSuffix()
还可以通过 with(Delegate ) 和 withArgThat(org.hamcrest.Matcher )自定义匹配。
Using the null
value to match any object reference
可以使用 null 去替代 any 或 withAny() 来配置任意值,但仅限于至少一个参数使用了 anyXx 或 withXx() ,否则它严格匹配 null 。 另外可用 withNull() 去匹配 null 值。
也去是说,如果有某个参数匹配采用弹性表达式,剩下的参数如果需要匹配 null 必须使用 withNull() 。
Matching values passed through a varargs parameter 匹配变长参数
如果有确定的个数,每个的值也确定,可以一一列出。 如果个数无所谓(包括0),可以((Object[]) any) 的方式。 如,(String[])any 。
不能组合使用严格值匹配和弹性匹配。
Specifying invocation count constraints 指定调用次数约束
在录制和验证阶段都可以指定;指定在对应的方法之后。
通过三个属性指定: times, minTimes 和 maxTimes 。 每个属性只能使用一次。
times=0 或 maxTimes=0 意味着一旦目标方法被调用测试就fail。
Explicit verification 显式的验证
如前文,对 mock方法调用的次数也可以在验证阶段指定,其语法规则和录制阶段是一样的。
因为 StrictExpectations 已经严格的表达了验证的要求,不能多也不能少,所以不能再有 Verifacations 块。
对与 regular的 Expectations , 因为它 record 的是最低要求,如果还有更多要求则需要在 Verifications 阶段补充。比如,不允许某个方法被调用,可以通过 times=0 指定。
Verification in order 顺序验证
如果需要验证方法 replay 的顺序,则使用 VerificationsInOrder 。
@Test public void verifyingExpectationsInOrder(@Mocked final DependencyAbc abc) { // Somewhere inside the tested code: abc.aMethod(); abc.doSomething("blah", 123); abc.anotherMethod(5); ... new VerificationsInOrder() {{ // The order of these invocations must be the same as the order // of occurrence during replay of the matching invocations. abc.aMethod(); abc.anotherMethod(anyInt); }}; }
注意这里, abc.doSomething() 没有验证,所以它是否出现及出现的顺序都无关紧要。
Partially ordered verification 局部顺序验证
可以通过 unverifiedInvocations() 隔离方法之间顺序的相关性。
@Mocked DependencyAbc abc; @Mocked AnotherDependency xyz; @Test public void verifyingTheOrderOfSomeExpectationsRelativeToAllOthers() { new UnitUnderTest().doSomething(); new VerificationsInOrder() {{ abc.methodThatNeedsToExecuteFirst(); unverifiedInvocations(); // Invocations not verified must come here... xyz.method1(); abc.method2(); unverifiedInvocations(); // ... and/or here. xyz.methodThatNeedsToExecuteLast(); }}; }
这里会验证 1,abc.methodThtNeedsToExecuteFirst() 是第一个调用 2,xyz.methodThatNeedsToExecuteLast() 是最后一个调用; 3,xyz.method1(); abc.method2() 依次调用;但在 unverifiedInvocations() 地方可能还有更多的方法调用,但无所谓。
有时,我们关心某几个方法的调用顺序,但剩下的几个方法只验证调用了,不要求顺序,这时可通过两个验证块来表达:
1 @Test 2 public void verifyFirstAndLastCallsWithOthersInBetweenInAnyOrder() 3 { 4 // Invocations that occur while exercising the code under test: 5 mock.prepare(); 6 mock.setSomethingElse("anotherValue"); 7 mock.setSomething(123); 8 mock.notifyBeforeSave(); 9 mock.save(); 10 11 new VerificationsInOrder() {{ 12 mock.prepare(); // first expected call 13 unverifiedInvocations(); // others at this point 14 mock.notifyBeforeSave(); // just before last 15 mock.save(); times = 1; // last expected call 16 }}; 17 18 // Unordered verification of the invocations previously left unverified. 19 // Could be ordered, but then it would be simpler to just include these invocations 20 // in the previous block, at the place where "unverifiedInvocations()" is called. 21 new Verifications() {{ 22 mock.setSomething(123); 23 mock.setSomethingElse(anyString); 24 }}; 25 }
上面即使去掉第13行,测试也通过。但却不能验证 22,23 行出现在13。 多个验证块时,它们的相对顺序很重要。
Full verification
严格验证被调用的方法有哪些个,不能多也不能少,但顺序无关。
1 @Test 2 public void verifyAllInvocations(@Mocked final Dependency mock) 3 { 4 // Code under test included here for easy reference: 5 mock.setSomething(123); 6 mock.setSomethingElse("anotherValue"); 7 mock.setSomething(45); 8 mock.save(); 9 10 new FullVerifications() {{ 11 // Verifications here are unordered, so the following invocations could be in any order. 12 mock.setSomething(anyInt); // verifies two actual invocations 13 mock.setSomethingElse(anyString); 14 mock.save(); // if this verification (or any other above) is removed the test will fail 15 }}; 16 }
注意如果最小次数验证在Expectations 中指定,则在 FullExpections 中无需再指定。 这个验证始终在测试结束前执行。
Full verification in order
@Test public void verifyAllInvocationsInOrder(@Mocked final Dependency mock) { // Code under test included here for easy reference: mock.setSomething(123); mock.setSomethingElse("anotherValue"); mock.setSomething(45); mock.save(); new FullVerificationsInOrder() {{ mock.setSomething(anyInt); mock.setSomethingElse(anyString); mock.setSomething(anyInt); mock.save(); }}; }
这里, mock.setSomething(anyInt) 必须指定两次。
Restricting the set of mocked types to be fully verified 只对指定的 mock 对象或类型做完全验证
@Test public void verifyAllInvocationsToOnlyOneOfTwoMockedTypes( @Mocked final Dependency mock1, @Mocked AnotherDependency mock2) { // Inside code under test: mock1.prepare(); mock1.setSomething(123); mock2.doSomething(); mock1.editABunchMoreStuff(); mock1.save(); new FullVerifications(mock1) {{ mock1.prepare(); mock1.setSomething(anyInt); mock1.editABunchMoreStuff(); mock1.save(); times = 1; }}; }
也就是传递 mock objects 或 Classes 做参数给 FullVerifications(...) , 即只关心指定的 mock 对象或类型。
Verifying that no invocations occurred
只需要使用空的 FullVerifications 块即可: new FullInvocations(){{}};
录制时具有隐含的验证功能,对于下例
new Expectations() {{ mock.a(); time=1; }} .... new FullVerifications(){{ }};
Expectations 块要求mock.a()只能被调用一次,但 FullVerifications 块则进而要求没有其他的方法调用。
如果多个 mock 类型或实例,而我们并不关心所有的,那只需在 FullVerifications 的参数列出感兴趣的类或实例就行了。
Test public void verifyNoInvocationsOnOneOfTwoMockedDependenciesBeyondThoseRecordedAsExpected( @Mocked final Dependency mock1, @Mocked final AnotherDependency mock2) { new Expectations() {{ // These two are recorded as expected: mock1.setSomething(anyInt); mock2.doSomething(); times = 1; }}; // Inside code under test: mock1.prepare(); mock1.setSomething(1); mock1.setSomething(2); mock1.save(); mock2.doSomething(); // Will verify that no invocations other than to "doSomething()" occurred on mock2: new FullVerifications(mock2) {}; }
这里表达了,除了 doSomething() 需要被调用一次之外,mock2 不能有其他方法调用。
Verifying unspecified invocations that should not happen
上面已经提到,如果要保证某些方法不被调用的话,不必在 Expectations / verification 块中一一指明 time=0 。 只要不在 FullVerification 块中提到那些方法就行了:
@Test public void readOnlyOperation(@Mocked final Dependency mock) { new Expectations() {{ mock.getData(); result = "test data"; }}; // Code under test: String data = mock.getData(); // mock.save() should not be called here ... new FullVerifications() {{ mock.getData(); minTimes = 0; // calls to getData() are allowed, others are not }}; }
Capturing invocation arguments for verification 获得录制阶段传入的参数
对参数验证往往是通过之前提到的参数匹配来实现的,这里提到了另外一种更直观的方式,把参数捕捉到然后做进一步的验证。
这时要使用 withCapture(...) 作为参数匹配器(意味着,此时只获得其值,不做匹配)。 有三种使用情况:
- 捕捉只调用一次的方法的参数 :T withCapture() , 使用 withCapture() 匹配参数,然后取返回值;
- 捕捉调用多次的方法的参数: T withCapture(List<T>) , 取传入的 list 参数;
- 捕捉构造方法的参数: List<T> withCapture(T)
捕捉只调用一次的方法的参数:
@Test public void capturingArgumentsFromSingleInvocation(@Mocked final Collaborator mock) { // Inside tested code: new Collaborator().doSomething(0.5, new int[2], "test"); new Verifications() {{ double d; String s; mock.doSomething(d = withCapture(), null, s = withCapture()); assertTrue(d > 0.0); assertTrue(s.length() > 1); }}; }
注意,这里的 withCapture() 只能用在Verifications 块中。 如果这个方法被调用多次,那拿到的是最后一次的值。
捕捉调用多次的方法的参数
@Test public void capturingArgumentsFromMultipleInvocations(@Mocked final Collaborator mock) { mock.doSomething(dataObject1); mock.doSomething(dataObject2); new Verifications() {{ List<DataObject> dataObjects = new ArrayList<>(); mock.doSomething(withCapture(dataObjects)); assertEquals(2, dataObjects.size()); DataObject data1 = dataObjects.get(0); DataObject data2 = dataObjects.get(1); // Perform arbitrary assertions on data1 and data2. }}; }
和前面不同,这里的 withCaptrue(List<T>) 也可以用在 Expectation 块中。
捕捉在回放过程中创建的 mock 实例:
1 @Test 2 public void capturingNewInstances(@Mocked Person mockedPerson) 3 { 4 // From the code under test: 5 dao.create(new Person("Paul", 10)); 6 dao.create(new Person("Mary", 15)); 7 dao.create(new Person("Joe", 20)); 8 9 new Verifications() {{ 10 // Captures the new instances created with a specific constructor. 11 List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt)); 12 13 // Now captures the instances of the same type passed to a method. 14 List<Person> personsCreated = new ArrayList<>(); 15 dao.create(withCapture(personsCreated)); 16 17 // Finally, verifies both lists are the same. 18 assertEquals(personsInstantiated, personsCreated); 19 }}; 20 }
Delegates: specifying custom results 定制result 的计算逻辑
我们之前已经看到如何通过 result 属性或 returns(...) 录制结果。 也看到了如何通过 anyXxx 属性或 withXxx() 方法来匹配方法参数。 那如何根据回放时传入的参数返回响应的结果呢,可以通过 Delegate 接口定制:
@Test public void delegatingInvocationsToACustomDelegate(@Mocked final DependencyAbc anyAbc) { new Expectations() {{ anyAbc.intReturningMethod(anyInt, null); result = new Delegate() { int aDelegateMethod(int i, String s) { return i == 1 ? i : s.length(); } }; }}; // Calls to "intReturningMethod(int, String)" will execute the delegate method above. new UnitUnderTest().doSomething(); }
此接口是个标志接口,没有方法。所以方法名可以随意; 参数有两种选择,要么和record的方法参数一致,要么没有参数,这两种选择都允许添加一个 Invocation 类型做为第一个参数。
通过 Delegate 也可以定制构造方法:
@Test public void delegatingConstructorInvocations(@Mocked Collaborator anyCollaboratorInstance) { new Expectations() {{ new Collaborator(anyInt); result = new Delegate() { void delegate(int i) { if (i < 1) throw new IllegalArgumentException(); } }; }}; // The first instantiation using "Collaborator(int)" will execute the delegate above. new Collaborator(4); }
Cascading mocks 级联 mock
这种情况: obj1.getObj2(..).getObj3().getObj4().doSomething(...) , 需要mock所有这链接的几个对象和类。
@Test public void recordAndVerifyExpectationsOnCascadedMocks( @Mocked Socket anySocket, // will match any new Socket object created during the test @Mocked final SocketChannel cascadedChannel // will match cascaded instances ) throws Exception { new Expectations() {{ // Calls to Socket#getChannel() will automatically return a cascaded SocketChannel; // such an instance will be the same as the second mock parameter, allowing us to // use it for expectations that will match all cascaded channel instances: cascadedChannel.isConnected(); result = false; }}; // Inside production code: Socket sk = new Socket(); // mocked as "anySocket" SocketChannel ch = sk.getChannel(); // mocked as "cascadedChannel" if (!ch.isConnected()) { SocketAddress sa = new InetSocketAddress("remoteHost", 123); ch.connect(sa); } InetAddress adr1 = sk.getInetAddress(); // returns a newly created InetAddress instance InetAddress adr2 = sk.getLocalAddress(); // returns another new instance ... // Back in test code: new Verifications() {{ cascadedChannel.connect((SocketAddress) withNotNull()); }}; }
因为Socket, SocketChannel 都被 @Mocked, 尽管没有录制 Socket#getChannel(), 此方法每次调用仍然会返回相同的 mocked SockecChannel 实例。 如果不存在一个mocked 实例类型与返回值匹配,那它每次都返回不同的的实例。
注意,方法若没有被录制,缺省返回空对象, 所有方法都是空实现的对象, 不是 null 。 但 String - null, Object - null, 集合-长度为0的集合。
Cascading static factory methods
这种级联mock的特性在mocked类型包含静态的工厂方法是尤为有用:
public void postErrorMessageToUIForInvalidInputFields(@Mocked final FacesContext jsf) { // Set up invalid inputs, somehow. // Code under test which validates input fields from a JSF page, adding // error messages to the JSF context in case of validation failures. FacesContext ctx = FacesContext.getCurrentInstance(); if (some input is invalid) { ctx.addMessage(null, new FacesMessage("Input xyz is invalid: blah blah...")); } ... // Test code: verify appropriate error message was added to context. new Verifications() {{ FacesMessage msg; jsf.addMessage(null, msg = withCapture()); assertTrue(msg.getSummary().contains("blah blah")); }}; }
你看,我们无需录制 FacesContext.getCurrentInstance(),它就可用。
Cascading self-returning methods
流式(Fluent )接口是另一个级联mock好用的情况:
@Test public void createOSProcessToCopyTempFiles(@Mocked final ProcessBuilder pb) throws Exception { // Code under test creates a new process to execute an OS-specific command. String cmdLine = "copy /Y *.txt D:\\TEMP"; File wrkDir = new File("C:\\TEMP"); Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start(); int exit = copy.waitFor(); ... // Verify the desired process was created with the correct command. new Verifications() {{ pb.command(withSubstring("copy")).start(); }}; }
这里 command(),directory() 和 inheritIO() 都返回同一个pb 对象, 最后start()返回一个新 mocked Process 对象。
Accessing private members 访问私有成员
有时我们需要访问被测对象和mocked对象的私有成员,Deencapsulation 提供了静态方法来解决这个问题:
import static mockit.Deencapsulation.*; @Test public void someTestMethod(@Mocked final DependencyAbc abc) { final CodeUnderTest tested = new CodeUnderTest(); // Defines some necessary state on the tested object: setField(tested, "someIntField", 123); new Expectations() {{ // Expectations still recorded, even if the invocations are done through Reflection: newInstance("some.package.AnotherDependency", true, "test"); maxTimes = 1; invoke(abc, "intReturningMethod", 45, ""); result = 1; // other expectations recorded... }}; tested.doSomething(); String result = getField(tested, "result"); assertEquals("expected result", result); }
这里用到了四个方法:setField()/getField(), newInstance() 和 invoke();
注意我们不仅仅通过反射访问被测类的私有成员,对mocked 类/实例的私有方法的录制和验证,通过反射调用也是可以的。 当然,通常不需要录制和验证私有方法,除非在只mock部分方法的场景下。
Partial mocking mock 部分方法
缺省的,@Mocked 标注的类型及其父类型(除了Object)的所有方法和构造方法都被mock。mock部分方法就是只有录制过的方法才被mock, 而其他的方法直接调用实际的实现。
public class PartialMockingTest { static class Collaborator { final int value; Collaborator() { value = -1; } Collaborator(int value) { this.value = value; } int getValue() { return value; } final boolean simpleOperation(int a, String b, Date c) { return true; } static void doSomething(boolean b, String s) { throw new IllegalStateException(); } } @Test public void partiallyMockingAClassAndItsInstances() { final Collaborator anyInstance = new Collaborator(); new Expectations(Collaborator.class) {{ anyInstance.getValue(); result = 123; }}; // Not mocked, as no constructor expectations were recorded: Collaborator c1 = new Collaborator(); Collaborator c2 = new Collaborator(150); // Mocked, as a matching method expectation was recorded: assertEquals(123, c1.getValue()); assertEquals(123, c2.getValue()); // Not mocked: assertTrue(c1.simpleOperation(1, "b", null)); assertEquals(45, new Collaborator(45).value); } @Test public void partiallyMockingASingleInstance() { final Collaborator collaborator = new Collaborator(2); new Expectations(collaborator) {{ collaborator.getValue(); result = 123; collaborator.simpleOperation(1, "", null); result = false; // Static methods can be dynamically mocked too. Collaborator.doSomething(anyBoolean, "test"); }}; // Mocked: assertEquals(123, collaborator.getValue()); assertFalse(collaborator.simpleOperation(1, "", null)); Collaborator.doSomething(true, "test"); // Not mocked: assertEquals(2, collaborator.value); assertEquals(45, new Collaborator(45).getValue()); assertEquals(-1, new Collaborator().getValue()); } }
mock 部分方法时,需要指定录制的方法影响某个实例还是类。取决于传给构造方法 Expectations(Object ...) 的参数是类还是实例。 如果是类,则类及基类的所有方法及构造方法都可以被录制。
一个对象在通过expectations 构造方法转化成mock对象之后,它之前的状态(field值)仍然保留。
这里总结下, 把类或对象变成mocked 有两种方式: 用 @Mocked 或做为 Expectations()的参数。前者即使不录制,其所有方法也同时被mock;在使用后者时,尽管类或实例转化成了mocked, 但其方法只有在录制之后方法才是mocked。
只要是mocked 类型或实例,其方法就可以被 verify 。即使这个方法没有mocked -- 没有录制。 这里和 Mockito 的spy对象的概念一致。
@Test public void partiallyMockingAnObjectJustForVerifications() { final Collaborator collaborator = new Collaborator(123); new Expectations(collaborator) {}; // No expectations were recorded, so nothing will be mocked. int value = collaborator.getValue(); // value == 123 collaborator.simpleOperation(45, "testing", new Date()); ... // Unmocked methods can still be verified: new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }}; }
最后补充一点,应用部分mocking 更简单的方式是把测试类的成员变量一并标注@Mocked 和@Tested, 这样,此变量不传给Expectations 的构造方法。但我们任然需要录制需要的方法。@Tested 意味着被测对象,两者组合就是把mocked 实例/类也当做被测类,从而可以测试它没有被mock的方法。
Capturing implementation classes and instances 捕捉所有的实现类和实例
下面这个例子并不实用, 但能明了地说明问题。看发行版随附的示例则更实用,可参考其中 Timing Framework 。
public interface Service { int doSomething(); } final class ServiceImpl implements Service { public int doSomething() { return 1; } } public final class TestedUnit { private final Service service1 = new ServiceImpl(); private final Service service2 = new Service() { public int doSomething() { return 2; } }; public int businessOperation() { return service1.doSomething() + service2.doSomething(); } }
businessOperation() 是我们要测试的目标方法。
目前的问题是,service1 和 service2 是 final 的属性,不能通过反射设置。我们需要在实例化被测实例前先 mock ServiceImple 和那个匿名类的 doSomething(), 但是匿名类,怎么mock呢!
Mocking unspecified implementation classes 使给定基类的子孙类被mock
@Capaturing 可以帮我们实现这个目标: 给定基类或接口,使其所有子类被 mock。
public final class UnitTest { @Capturing Service anyService; @Test public void mockingImplementationClassesFromAGivenBaseType() { new Expectations() {{ anyService.doSomething(); returns(3, 4); }}; int result = new TestedUnit().businessOperation(); assertEquals(7, result); } }
上例,record 阶段两个返回值指定给 Service#doSomething(), 在replay阶段,不管实现类或实例是哪个,只要调用这个方法都会受到影响。
Specifying behavior for future instances 限定受 @Capturing 影响的实例个数
@Capturing 有个int类型可选属性 "maxInstances" 可以限制受影响的实例个数;这样我们就可以为同一基类的多个mock实例 record 或 verify 不同的行为。这可以通过定义2到多个@Capturing fileds/parameters, 每个都指定 maxInstance , 当然最后一个可以没有。
下面实例仅仅为了方便理解,并无实际用途:
@Test public void testWithDifferentBehaviorForFirstNewInstanceAndRemainingNewInstances( @Capturing(maxInstances = 1) final Buffer firstNewBuffer, @Capturing final Buffer remainingNewBuffers) { new Expectations() {{ firstNewBuffer.position(); result = 10; remainingNewBuffers.position(); result = 20; }}; // Code under test creates several buffers... ByteBuffer buffer1 = ByteBuffer.allocate(100); IntBuffer buffer2 = IntBuffer.wrap(new int[] {1, 2, 3}); CharBuffer buffer3 = CharBuffer.wrap(" "); // ... and eventually read their positions, getting 10 for // the first buffer created, and 20 for the remaining ones. assertEquals(10, buffer1.position()); assertEquals(20, buffer2.position()); assertEquals(20, buffer3.position()); }
需要注意,只要有 @Capturing 修饰,所有实例都是mocked, 尽管它可能没有被录制。
Instantiation and injection of tested classes 实例化测试类及对其注入依赖
往往,一个测试类往往操作一个被测的类。JMockit 提供的@Tested 可以自动实例化这个类并注入mocked依赖。
@Tested 的属性不能是final的,其实例化和依赖注入发生在测试方法执行前,此时若属性是null,则实例化和依赖注入就会执行,否则略过。
依赖注入的mock需要测试类通过@Injectable 的属性或参数提供,@Mocked 或 @Capturing 没有这个功能。另外,非mock的数据也可以注入,如原始类型或数组。
public class SomeTest { @Tested CodeUnderTest tested; @Injectable Dependency dep1; @Injectable AnotherDependency dep2; @Injectable int someIntegralProperty = 123; @Test public void someTestMethod(@Injectable("true") boolean flag, @Injectable("Mary") String name) { // Record expectations on mocked types, if needed. tested.exerciseCodeUnderTest(); // Verify expectations on mocked types, if required. } }
注意,@Injectable 的数据要是非mock的,应该被显式的赋值。在@Injectable 标注参数时,赋值可以通过 @Injectable 的value 属性。
支持构造方法注入和字段注入。对于前者,能要能被 @Injectable 的变量匹配上。 需注意,对于一个给定的测试方法,可注入的变量是@Injectable fileds和这个测试方法@Injectable 参数的组合。因此,不同的测试方法可以提供不同的要注入的参数组合。
在被测试的类型通过被选择的构造器初始化之后,就开始注入它的非final的属性。对于每个要注入的属性来说,它会直接复制测试类中相同类型的@Injectable 变量,如果相同类型的有多个,就根据名称匹配。
Reusing expectation and verification blocks
最简单的办法就是把mock对象定义成属性。
public final class LoginServiceTest { @Tested LoginService service; @Mocked UserAccount account; @Before public void init() { new NonStrictExpectations() {{ UserAccount.find("john"); result = account; }}; } @Test public void setAccountToLoggedInWhenPasswordMatches() throws Exception { willMatchPassword(true); service.login("john", "password"); new Verifications() {{ account.setLoggedIn(true); }}; } private void willMatchPassword(final boolean match) { new Expectations() {{ account.passwordMatches(anyString); result = match; }}; } @Test public void notSetAccountLoggedInIfPasswordDoesNotMatch() throws Exception { willMatchPassword(false); service.login("john", "password"); new Verifications() {{ account.setLoggedIn(true); times = 0; }}; } // other tests that use the "account" mock field }
两个方法都要测试 LoginService#login(String accountId, String password)
method。它尝试去通过"accountId"查询得到用户账户,因为两个测试都依赖这个方法,所以提过一个 NonStrictExpectation 录制它。记住,一个测试可以有多个 expectations/verifications 块,当样的块也可以在共享的 "before", "after" 方法内。这里还显示了把一个公共的 expectations 抽成一个方法,以备给所有的测试方法共享。
我们还可以创建Expectations 的子类来完成相同的重用:
final class PasswordMatchingExpectations extends Expectations { PasswordMatchingExpectations(boolean match) { account.passwordMatches(anyString); result = match; } } @Test public void setAccountToLoggedInWhenPasswordMatches() throws Exception { new PasswordMatchingExpectations(true); ... }
自定义的扩展类通常应是 fianl 的,除非打算再做扩展,在那种情况下,类名称必须以 "Expectations" 或 "Verifications" 结尾,否则 JMockit 无法识别。
Other topics 其他主题
Mocking multiple interfaces at the same time mocked 对象同时实现多个接口
下面示例一个mocked 对象怎样实现多个接口:
public interface Dependency { String doSomething(boolean b); } public class MultiMocksTest<MultiMock extends Dependency & Runnable> { @Mocked MultiMock multiMock; @Test public void mockFieldWithTwoInterfaces() { new Expectations() {{ multiMock.doSomething(false); result = "test"; }}; multiMock.run(); assertEquals("test", multiMock.doSomething(false)); new Verifications() {{ multiMock.run(); }}; } @Test public <M extends Dependency & Serializable> void mockParameterWithTwoInterfaces( @Mocked final M mock) { new Expectations() {{ mock.doSomething(true); result = "abc"; }}; assertEquals("abc", mock.doSomething(true)); assertTrue(mock instanceof Serializable); } }
核心是这样的语法 <MM extends A & B> 。 然后利用 MM 声明mock 的类型。
Iterated expectations 录制迭代过程
使用 strict expectations 录制的连续的调用过程缺省要求的执行次数是1,我们也可以要求迭代次数,通过 StrictExpectations(numberOfIterations)
@Test public void recordStrictInvocationsInIteratingBlock(@Mocked final Collaborator mock) { new StrictExpectations(2) {{ mock.setSomething(anyInt); mock.save(); }}; // In the tested code: mock.setSomething(123); mock.save(); mock.setSomething(45); mock.save(); }
Expectations 和 NonstrictExpectations 也可以使用这种方式。迭代次数仅仅作为一个最大/最小次数限制的乘数。对于 NonstrictExpectations 来说
Verifying iterations 验证迭代次数
同样的 Verifications 构造方法也可以指定迭代次数
@Test public void verifyAllInvocationsInLoop(@Mocked final Dependency mock) { int numberOfIterations = 3; // Code under test included here for easy reference: for (int i = 0; i < numberOfIterations; i++) { DataItem data = getData(i); mock.setData(data); mock.save(); } new Verifications(numberOfIterations) {{ mock.setData((DataItem) withNotNull()); mock.save(); }}; new VerificationsInOrder(numberOfIterations) {{ mock.setData((DataItem) withNotNull()); mock.save(); }}; }
例中使用了两种 verifications,仅仅是为了说明两者的区别。对于前者,这个次数参数仅仅作为一个乘数,它与显式的次数(times,minTimes,maxTimes)的积构成是实际的次数要求。而对于 VerificationsInOrder, 因为显式的次数设置对它无效, 所以这个参数相当于把这个顺序的调用过程重复n次 。
同样,这个次数参数在 FullVerifications 及 FullVerificationsInOrder 上应用可以类比这个例子。