代码改变世界

JMockit 指南 翻译

2016-01-12 00:13  yoogo  阅读(2088)  评论(0编辑  收藏  举报

原文 

翻译并没有完全遵循原文,有个人的理解与提炼。 如果你觉得有什么地方生涩难懂,欢迎反馈。谢谢! 

更新时间: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标注:

  1. @Mocked , mock 类及其基类。
  2. @Injectable , mock 类的某个实例,类的其它实例不受影响。
  3. @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技术的基于行为的测试中,这三个阶段正好对应到:

  1. 录制 - 预期mock 对象可能被调用的方法, 及其应返回的结果;
  2. 回放 - 被测对象执行,调用 mock 对象的方法;
  3. 验证 - 检查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(...) 作为参数匹配器(意味着,此时只获得其值,不做匹配)。 有三种使用情况:

  1. 捕捉只调用一次的方法的参数 :T withCapture() , 使用 withCapture() 匹配参数,然后取返回值;
  2. 捕捉调用多次的方法的参数: T withCapture(List<T>) , 取传入的 list 参数;
  3. 捕捉构造方法的参数: 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 上应用可以类比这个例子。