模拟

  1. 模拟类型和实例
  2. 期望
  3. 录制和验证的模式
  4. 普通和严格的期望
    1. 严格和非严格的模拟
  5. 记录所期望的结果
  6. 匹配特定实例的调用
    1. 注入模拟实例
    2. onInstance(m) 约束
    3. 使用给定构造方法新建实例
  7. 对参数值得灵活匹配
    1. 使用any 属性匹配参数
    2. 使用with 方法进行参数匹配
    3. 使用 null 值匹配所有任何其他对象的引用、
    4. 通过 varargs 参数匹配参数的传入
  8. 定义调用的次数约束
  9. 显式验证
    1. 验证调用从来没有发生
    2. 验证顺序
    3. 部分顺序的验证
    4. 全验证
    5. 按顺序进行全验证
    6. 限制模拟类型集合进行全验证
    7. 验证没有调用发生
    8. 验证未定义的调用不能发生
  10. 捕捉验证时的调用参数
    1. 从单个调用中捕捉参数
    2. 从多个调用中捕捉参数
    3. 捕捉新实例
  11. 代理:定义自己的结果
  12. 级联模拟
    1. 级联静态工厂方法
    2. 级联自返回的方法
  13. 部分模拟
  14. 捕捉实现类和实例
    1. 模拟未明确实现的类
    2. future 实例的定义行为
  15. 实例化和注入测试对象
  16. 重用期望和验证代码块
  17. 其他主题
    1. 同时模拟多个接口
    2. 迭代期望
    3. 验证迭代

在 JMockit 工具中, 期望 API 对使用模拟在自动化测试开发中提供了丰富的支持。当模拟被使用的时候,一个测试关注于测试代码的结果是否与它交互的其他类型一致。模拟通常被适用于独立的单元测试的构造中,既一个测试单元被相对于独立其他的所依赖的单元实现。通常来说,一个单元的行为被嵌入到单个类中,但是它被适用于一个具有很强关联的类的集合中,作为一个独立的测试单元 (通常来说这种情况下,我们包含一个中心的类包含一个或者是多个帮助类,可能是包内私有的); 通常来说,独立的方法不应该被当做一个他们分开的单元。

然而严格的单元测试不是所推荐的方式;不需要尝试模拟所有独立的依赖。模拟最好被适度的应用;任何可能的,更好的情况下集成测试远胜于单独的单元测试。这也就是说,模拟只在偶尔建立集成测试的时候比较有用,当有一个特殊的依赖不能被很容易的建立实现或者是当尝试建立对边缘测试的时候能够起到很好的作用。

在两个类之间的交互通常采用一个方法或者是构造方法的调用。从一个测试类到它依赖的一些列调用,包括参数和返回值在他们之间,定义了对特殊类在测试中的有兴趣的行为。此外,一个给定的测试可能需要验证在多个调用中的相对顺序。

模拟类型和实例

在测试中对一个依赖的方法和构造方法的调用是模拟的对象。模拟提供了我们需要独立测试代码从他们的依赖中的机制。我们指定为一个给定测试的特殊的依赖被模拟通过申明合适的模拟属性和或者是模拟参数; 模拟属性被使用注释在测试类属性上进行申明, 然而模拟参数要在一个测试方法的参数上使用注释申明。 需要被模拟的依赖的类型可以使模拟属性或是参数。这个类型可以是任何类型的引用: 一个接口,一个类 (包含抽象和最后的类型), 一个注释,一个枚举。

缺省的,所有的模拟类中的非私有方法 (包括任何的静态,最终或者是本地) 可以在测试运行时被模拟。 如果被申明的模拟类型是一个类,所有的它父类不包括 java.lang.Object 都将被递归的模拟。因此,继承的方法都将被自动的模拟。同样的在这个类中的所有非私有构造方法都将被模拟。

当一个方法或者是构造方法被模拟的时候,它原来的实现代码就不会在测试调用的时候执行。而是调用被重定向到 JMockit中,因此它能够被按照隐式或显式的定义的方法进行测试。

如下的例子测试骨骼服务作为一个基础插图正如他们被测试代码所使用的那样,使用了模拟属性和模拟参数的申明。在这个教程中,我们使用很多代码片段,黑体部分是我们主要关注的说明。

// "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;
   }};
   ...
}

作为一个被申明的模拟参数在测试方法中,申明类型的实例将会被自动被JMockit建立和通过JUnit/TestNG运行期自动传入,当执行到这些测试方法的时候; 因此,参数值不可能为 null。 对于一个模拟属性,被申明类型的实例将被JMockit 自动建立和赋值给属性,确保其不是不可变的。

对于模拟属性和参数有一些注释申明上的不同,缺省的模拟行为可以被修改来满足特殊的测试。这个章节的其他部分将详细讨论这些,但是基础是: @Mocked 是中心模拟注释,包含一些可选的属性在特殊情况下比较有用; @Injectable 是另外一个模拟注释,其约定只模拟单个模拟实例中的实例方法; 而 @Capturing 是另外一个模拟注释, 其扩展到实现模拟接口的类, 或者是继承自模拟类的子类。 当 @Injectable 或 @Capturing 被应用到一个模拟属性或者是模拟参数的时候,, @Mocked 就被隐式的使用了,不需要再使用 (但是也可以) 。

JMockit 建立的模拟实例能够正常的在测试代码中使用(为了录制和验证期望), 和/或 传递到测试代码中。或者他们可能简单的不被使用。于其他的 mocking APIs不同的是, 这些模拟对象不需要被测试代码使用,当调用了依赖中的实例方法的时候。缺省的 (例如,当 @Injectable 不被使用的时候), JMockit 不关系哪些模拟实例方法被调用。这允许透明的模拟实例能够在测试代码中直接建立,当说到代码调用构造方法构建新的实例使用new操作符;这些实例化的类不需要被所申明的模拟类型所覆盖, 就这么多了。

期望

期望是与给定测试相关的需要被模拟的方法/构造方法的调用集合。一个期望可能覆盖多个对相同方法或构造方法的不同调用,但是它不需要覆盖在测试执行是偶的所有可能调用。是否一个特殊的调用匹配给定的期望或是不仅仅依赖于方法/构造方法的签名,还需要包括方法调用的实例,参数值,和或者是已经被匹配的调用数目。因此,有不同类型的匹配约束可选的别定义在给定的期望中。

当我们有一个或者是多个调用参数需要使用的时候,一个确定的参数值可能需要针对每个参数进行定义。例如,字符串 "test string" 可能被定义为一个字符串参数, 导致期望只匹配当真实的参数值是这个的时候。正如我们后来看到的,不仅仅可以定义准确的值,我们还可以定义更多灵活的约束可能匹配所有的不同参数值集合。

下面将会显示一个针对 Dependency#someMethod(int, String)的期望, 只会匹配确切的参数值的方法调用。注意这个期望只定义通过一个独立对模拟方法的调用。没有特殊的 API 方法 methods 涉及, 正如其他的模拟 APIs一样。 但是这个调用却没有计算一个我们所感兴趣的测试中的真是调用次数。只是简单的定义了期望。

@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.
}

在我们深入了解在录制,重放和调用验证不同之后,我们将看到更多关于期望的使用。

录制和验证的模式

任何一个开发测试都可以被分成至少三个不同的独立执行阶段。阶段顺序被执行,一次只执行一个,如下所示。

@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.
   ...
}

首先,我们有一个准备阶段,这里需要的对象和数据项目被创建或者是从一些地方获取。然后,测试代码被执行,从测试代码中执行之后的结果和预期的值进行比较。

这个三阶段模式也被称为安排,行动,断言语法,或者是简称为AAA.不同的单词,但是意思是共同的。

在基于模拟类型的行为测试的上下文中 (和他们的模拟实例), 我们可能定义如下的其他阶段, 相对于之前描述的三个常用测试阶段:

  1. 录制阶段,这里调用被录制。这个发生在测试准备阶段,在我们需要的测试被执行之前。
  2. 重放阶段,这时所感兴趣的模拟调用有机会被执行,正是测试代码被执行时。之前被模拟的方法/构造方法调用将被重新再现。虽然,通常没有从调用录制到重放的一一对应。
  3. 验证阶段,在这里调用被验证是否和期望的一样。这通常发生在测试验证,在测试调用之后被执行。 基于行为的测试在 JMockit 中写起来,满足如下的模板:
import mockit.*;
... other imports ...

public class SomeTest
{
   // Zero or more "mock fields" common to all test methods in the class:
   @Mocked Collaborator mockCollaborator;
   @Mocked AnotherDependency anotherDependency;
   ...

   @Test
   public void testWithRecordAndReplayOnly(mock parameters)
   {
      // Preparation code not specific to JMockit, if any.

      new Expectations() {{ // an "expectation block"
         // One or more invocations to mocked types, causing expectations to be recorded.
         // Invocations to non-mocked types are also allowed anywhere inside this block
         // (though not recommended).
      }};

      // Unit under test is exercised.

      // Verification code (JUnit/TestNG assertions), if any.
   }

   @Test
   public void testWithReplayAndVerifyOnly(mock parameters)
   {
      // Preparation code not specific to JMockit, if any.

      // Unit under test is exercised.

      new Verifications() {{ // a "verification block"
         // One or more invocations to mocked types, causing expectations to be verified.
         // Invocations to non-mocked types are also allowed anywhere inside this block
         // (though not recommended).
      }};

      // Additional verification code, if any, either here or before the verification block.
   }

   @Test
   public void testWithBothRecordAndVerify(mock parameters)
   {
      // Preparation code not specific to JMockit, if any.

      new Expectations() {{
         // One or more invocations to mocked types, causing expectations to be recorded.
      }};

      // Unit under test is exercised.

      new VerificationsInOrder() {{ // an ordered verification block
         // One or more invocations to mocked types, causing expectations to be verified
         // in the specified order.
      }};

      // Additional verification code, if any, either here or before the verification block.
   }
}

还包括一些与以上模板不同之处,但是实质上就是期望属于录制阶段在测试代码被执行之前,验证代码块属于验证阶段。一个测试可以包含任意多个期望代码块,也可以没有。针对验证代码块也是如此。

实际上匿名内部类被用来是允许代码块可以被分割,充分使用当代 Java IDEs的代码折叠功能。如下的图片显示了在 IntelliJ IDEA的样子。

普通和严格的期望

期望被录制在 "new Expectations() {...}" 块中是通常的方式。 这意味着我们所期待的调用至少在重放阶段发生一次; 他们可能发生不只一次, 虽然可以和其他的录用期望的相对顺序不同; 此外, 不匹配任何录制期望是可以以任何个数和任意顺序。如果没有任何调用匹配上给定的期望,一个调用缺失的错误将会在测试结束之后抛出, 导致其失败 (这是仅有的缺省的行为,虽然这可以被重写)。

API 也支持严格期望的概念: 指的是当我们录制的时候只允许在重放的调用必须准确的匹配录制(当需要的时候,可以通过显式的定义允许), 无论是匹配的调用次数(缺省是只有一次)还是他们的发生顺序。在重放阶段的调用如果没有按照严格录制阶段中所期待的那样将会失败,迅速导致一个 "unexpected invocation" 错误, 结果测试失败。 这是通过使用 StrictExpectations 子类来实现的。

注意在严格期望的情况下,所有在重放阶段的调用匹配录制的期望都是隐式验证的。任何余下的调用不匹配一个期望都将被认为是不可预期的,导致测试失败。如果任何录制的严格期望确实了,测试也是失败的,比如,在重放节点没有匹配任何调用。

我们可以混合不同级别的严格条件在相同的测试中通过使用多个期望块,一些是普通的 (使用 Expectations), 其他是严格的 (使用 StrictExpectations)。 尽管通常来说,一个给定的模拟属性或模拟参数将会在一种类型的期望出现。

大多数的测试都会简单的使用普通的期望。使用严格的期望更可能是一个个人喜好问题。

严格和非严格的模拟

注意我们不需要定义给定的模拟类型/实例是严格或不是。相反的,一个给定的模拟属性/参数是否严格是通过它在测试中如何使用来决定的。一旦第一个严格期望被录制以 "new StrictExpectations() {...}" 代码块的形式, 相关的模拟类型/实例将被认为是在整个测试中严格的;否则就不是严格的。

记录所期望的结果

对于一个包含返回类型的方法,一个返回值可以通过给result属性赋值的方式录制。当在重放阶段中方法被调用的,所定义的返回值将会被返回给调用者。对 result的赋值应该正确的出现在期望代码块中的给定录制调用之后。

如果测试需要一个异常或错误被抛出在方法执行之后,result的属性还是可以被使用: 简单的赋值所需要的抛出的异常实例给它。注意录制的要被抛出的异常/错误需要符合模拟方法(任何返回类型),也包括模拟的构造方法。

多个连续的结果(返回值或抛出的异常)可以在同一个期望中被录制,通过简单的多次设置值在一行中。多个返回值和或者异常和错误的录制可以在同一个期望中自由的混合。在一个简单的对返回函数的调用中,可以给定期望中录制多个连续返回结果的例子。同样的,一个简单的对结果属性的赋值也能达到这个效果,如果赋值的是一个列表或者是数组包含连续的值。

以下的示例测试录制针对一个模拟依赖 DependencyAbc 类的方法的多个类型结果,当他们在 UnitUnderTest 类的测试调用中将会被使用。让我们看看在测试下的这个类的实现:

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
      }
   }
}

一个针对 doSomething() 方法的可能测试将抛出 SomeCheckedException,在一个连续的随意值的迭代中。假设我们想要(无论什么原因)录制一个完整的期望集合来与其他两个类交互,我们可能编写一下的测试。(通常,定义所有需要模拟方法的调用和特殊的模拟构造方法在给定的测试中是不需要或重要的。我们将会之后举个例子。)

@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();
}

这个测试录制了三个不同的期望。第一个通过调用 DependencyAbc() 构造方法来使用的, 仅仅为了表示在测试代码下如何通过无参数的构造方法进行初始化; 这个调用不需要定义结果, 除了偶尔需要抛出错误或异常 (构造方法不包含返回类型,因此录制从它们中返回值是没有意义的)。第二个期望定义了 调用 intReturningMethod() 方法将会返回3。 第三个定义了一些列三个连续的结果针对 stringReturningMethod() 方法, 这里最后一个结果是所需异常的实例, 允许测试来实现这个目标 (注意这个测试只会在异常不被抛出才通过)。

匹配特定实例的调用

之前,我们已经解释了如何在模拟实例上录制期望,例如 "abc.someMethod();" 将会准备的匹配 对 DependencyAbc#someMethod()方法的调用在任何依赖 DependencyAbc 类的实例上。在大多数的例子中,测试代码使用了一个独立的依赖实例,因此无论是否模拟实例可以在测试下被传递或创建是无关紧要且安全忽略的。 但是我们需要验证对指定实例的调用,在测试代码下被使用的一些实例中 ? 同样的, 是否一个或者是一些模拟对象的实例将被真的被模拟, 相同类的其他实例没有被模拟?(第二个例子将标准java类库或者是从第三方的库中类进行模拟是很普遍的。) JMockit 提供了模拟注释 , @Injectable, 只会模拟被模拟类型的一个实例, 而不会影响其他的实例。 此外,它还提供了一些方法来限制期望的匹配针对特定的 @Mocked 实例, 而不是模拟所有被模拟类的实例。

注入模拟实例

假设我们需要测试和多个给定类实例一起工作的代码,其中有我们需要模拟的部分。如果一个实例需要被模拟进行传递或者是注入到测试代码中,我们需要申明一个 @Injectable 模拟属性或者是模拟参数。 由JMockit建立的@Injectable 实例将是一个排他的模拟实例;任何其他相同模拟类型实例除非从一个分隔开的模拟属性/参数中获取,将仍然是一个正常的,非模拟实例。

同样需要注意,由于注入的模拟实例只能影响到对应那个实例的行为,因此静态方法和构造方法同样没有被模拟。毕竟一个静态方法是和改类的任何实例无关的,而构造方法只与新建的(和因此不同的) 实例相关.

例如,我们有如下的类需要被测试。

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();
   }
}

这个类可以很容易的被测试,不需要通过使用 ByteArrayInputStream 对象进行模拟输入,但是我们需要保证在构造方法中 InputStream#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 是很有必要的,因为在测试中的类继承了模拟类,对 ConcatenatingInputStream类的方法调用确实定义在 InputStream 基类中。 如果 InputStream 被普通的模拟, read(byte[]) 方法将会被模拟, 无论在任何调用它的实例中。

onInstance(m) 约束

当使用 @Mocked 或 @Capturing (和不是 @Injectable 在相同的模拟属性/参数中), 我们可以匹配重放对期望中录制的调用在特定的模拟实例上。比如,我们使用 onInstance(mockObject) 方法当录制期望的时候, 正如下面的例子所示。

@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());
}

以上的测试只会在测试代码(这里简短的嵌入到测试方法中) 调用 getValue() 在和录制调用时候相同的实例上才会通过。 这对于测试代码中多两个或者是更多的相同类型下的实例进行调用和测试想要验证每一个调用都发生在恰当的实例上的时候是很有用的。

为了避免在测试中使用多种不同的方法在多个相同类型的实例的每一个期望中使用 onInstance(m)方法, JMockit 自动引用 "onInstance" 的匹配基于模拟类型集合的范围内。 特别是,当有两个或者是更多的模拟属性/参数在一个给定测试范围中是相同类型情况下,对他们实例调用实例方法,将总是匹配到期望录制中相同的实例中。 因此,在这种普通的情况下,是不需要显示的使用 onInstance(m) 方法。

使用给定构造方法新建实例

特殊的 future 实例将会之后被测试代码创建 , JMockit 提供了一系列的机制来匹配在它们上面的调用。所有的机制都需要在特定的模拟类上的构造方法调用中录制期望(一个新的表达式)。

第一个机制仅仅使用从录制的构造期望中获取新实例,当在实例方法进行期望的时候。让我来看例子。

@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
   ...
}

在上面的测试中,我们申明了一个独立的模拟属性或模拟参数在所需的类上,使用 @Mocked 注释。 然而模拟的属性/参数却没有在录制期望的时候使用; 而是使用初始化的实例录制进一步在实例方法上的期望。 匹配上的构造方法调用建立future 实例将映射到对应的录制实例。 同样的,不需要一对一映射,而是可以多对一的映射,从潜在在多个 future 实例对应一个录制期望中使用的单个实例。

第二个机制中让我们能够录制一个替代的实例针对那些匹配了录制中的构造方法的实例。通过这种机制,我们可以重写以上的例子。

@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
   ...
}

两个版本的测试是等价的。第二个对实际的(非模拟的)用来替换的实例也是允许的,当和部分模拟组合使用的时候。

对参数值得灵活匹配

在录制和验证阶段,一个对模拟方法或构造方法的调用被定义在期望中。如果方法/构造方法包含一个或者是多个参数,然后录制/验证期望像某些东西(1, "s", true); 只会在重放阶段匹配相等的参数值得调用。对于那些普通的对象的参数 (非原始或是数组), equals(Object) 方法将被用来相等检测。 针对数组类型的参数, 相等检测扩充到每一个元素中; 因此,两个不同的数组实例在相同的维度中包含相同的长度和对应的元素也是相等的,则被认为相等。

在给定的测试中,我们可能不知道其中的参数值到底是什么,或者他们不是需要测试所关注的。因此,可以允许一个录制或验证调用可以匹配一系列重放的调用使用不同的参数值,我们可以灵活的定义参数匹配而不是实际参数值。这可以通过使用 anyXyz 属性或者是 withXyz(...) 方法来进行。 "any" 属性和"with" 方法都在 mockit.Invocations中定义, 该类是所有在测试中被使用的 expectation/verification 类的基类; 因此,他们既可以在期望中也可以在验证代码块中使用。

使用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" 属性必须出现在调用语句中确切的参数位置。你也可以使用普通的参数值针对其他的参数在相同的调用中。更具体的说明,可以查看 API 文档。

使用with 方法进行参数匹配

当录制或者验证一个期望的时候,withXyz(...) 方法可以在任何调用参数值传递的地方进行使用。 它们可以自由的和普通参数值传递混合使用(使用文本值 , 本地参数, 等等。)。唯一需要的是这些调用必须出现在录制/验证调用语句中出现,而不是在他们之前。比如首次对withNotEqual(val)调用的参数值给一个本地变量,然后在调用语句中使用参数是不可能的。一个测试例子使用 "with" 方法如下所示。

@Test
public void someTestMethod(@Mocked final DependencyAbc abc)
{
   final DataItem item = new DataItem(...);

   new Expectations() {{
      // Will match "voidMethod(String, List)" invocations with the first argument
      // equal to "str" and the second not null.
      abc.voidMethod("str", (List<?>) withNotNull());

      // Will match invocations to DependencyAbc#stringReturningMethod(DataItem, String)
      // with the first argument pointing to "item" and the second one containing "xyz".
      abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
   }};

   new UnitUnderTest().doSomething(item);

   new Verifications() {{
      // Matches invocations to the specified method with any long-valued argument.
      abc.anotherVoidMethod(withAny(1L));
   }};
}

好包含更多的 "with"方法不仅仅如上所示。可以通过查看 API 文档了解更多详细信息。

除了包含一些预定义在API中的约束匹配,JMockit 允许用户提供自定义的约束, 通过使用 with(Delegate) 和d withArgThat(Matcher) 方法。

使用 null 值匹配所有任何其他对象的引用、

当在一个期望中使用至少一个参数匹配方法或属性,我们可以使用快捷的方式来定义任何对象引用可以被接受的 (针对引用的参数类型)。简单的传递 null 值而不是 withAny(x) 或任何的参数匹配。 特别之处在于这允许不需要将值强制转成对应所申明的参数类型。然而, 牢牢记住这个特征只使用于至少一个明确参数匹配被使用于期望中(无论是 "with" 方法或一个 "any" 属性) 。当传递的调用中不包含任何匹配, null 值将只会匹配 null 引用。在之前的测试中,因此我们可以重写为:

@Test
public void someTestMethod(@Mocked final DependencyAbc abc)
{
   ...
   new Expectations() {{
      abc.voidMethod(anyString, null);
   }};
   ...
}

当定义对一个给定参数接受null引用的验证时,withNull() 匹配可以被使用。

通过 varargs 参数匹配参数的传入

偶尔我们需要处理可变长参数的方法或构造函数的期望。可以通过传递普通的值作为可变参数的参数值, 也可以通过使用"with"/"any" 匹配针对这些值。然而,却不可以在相同的期望中组合两种类型的值传递, 当目标是一个可变参数时。我们不是用普通的值就是用那些参数匹配器中的值。

当我们需要匹配可变长参数接受任意数量的值的调用时(包含 0), 我们可以定义期望使用 "(Object[]) any" 约束 final 的可变长度参数。

可能最好的方式用来明白确切的可变长参数匹配就是查看实际的测试代码 (由于没有涉及确切的 API)。测试类实际上展示了所有可能。

定义调用的次数约束

到目前为止,我们已经看到了除了可以关联方法或构造方法,一个期望可以包含调用结果和参数值匹配。在给定的测试代码中可以调用相同的方法或构造方法多次使用不同的或相同的参数,我们有时需要一个方法对所有的这些分隔的调用进行计数。

对一个允许的匹配上的给定期望中的调用次数可以通过调用次数约束进行定义。模拟 API提供了三种不同类型的属性来定义: 次数, 最少次数, 和最大次数。这些属性不仅仅可以用在录制阶段还可以被用在验证期望中。在任何中的一个例子中, 和期望相关的方法或构造方法将被约束在一个接受次数范围的区间内。 任何调用必须各自的少于或对于给定所期望的最少或上限, 和测试执行将会自动失败。让我们来查看一些例子。

@Test
public void someTestMethod(@Mocked final DependencyAbc abc)
{
   new Expectations() {{
      // By default, at least one invocation is expected, i.e. "minTimes = 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(@Mocked 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 被定义, 在重放节点的首次匹配调用中将导致测试失败(如果 任何)。

显式验证

除了可以定义调用次数的约束在录制期望阶段,我们还可以显式的在验证代码块中验证匹配的调用,在测试代码执行之后。针对于普通的期望是可用,但是对于严格的期望是不行的,因为他们总是隐式的验证; 在显式的验证块中进行重新校验是没有意义的。

在 "new Verifications() {...}" 块中我们可以使用相同的 API,和在 "new Expectations() {...}" 块中一样有效的, 使用异常方法或属性用来录制返回值和抛出异常/错误。也就是说, 我们可以自由的使用 anyXyz 属性, withXyz(...) 参数匹配方法, 和次数, 最小次数, 和醉倒调用次数的约束属性。 如果是一个测试例子。

@Test
public void verifyInvocationsExplicitlyAtEndOfTest(@Mocked final Dependency mock)
{
   // Nothing recorded here, though it could be.

   // Inside tested code:
   Dependency dependency = new Dependency();
   dependency.doSomething(123, true, "abc-xyz");

   // Verifies that Dependency#doSomething(int, boolean, String) was called at least once,
   // with arguments that obey the specified constraints:
   new Verifications() {{ mock.doSomething(anyInt, true, withPrefix("abc")); }};
}

注意,缺省的一个验证检测在重放至少包含一次的匹配调用。当我们需要验证确切的调用次数时(包括 1), times = n 约束必须被定义。

验证调用从来没有发生

为了在一个验证块中做这些,添加 "times = 0" 赋值在那些被期望在重放阶段不被使用的调用之后。如果一个或更多的匹配调用发生了,测试将失败。

验证顺序

普通的使用Verifications类建立的验证块是无序的。实际的相对顺序在 aMethod() 和anotherMethod() 方法在重放中被调用时不被验证, 但是每一个方法必须至少执行一次。如果你想要验证相对的调用顺序, "new 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(...) 调用在测试中没有验证, 因此它可以发送在任何时候(或根本不发送)。

部分顺序的验证

加入你想验证一个特别的方法 (或构造方法) 在其他调用之前/之后, 但是你不关系其他方法调用的顺序。 在顺序验证的块中,这可以通过简单的调用 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();
   }};
}

上面的例子确实有些复杂,正如它验证一些事情: a) 一个方法必须在其他之前; b) 一个方法必须在其他之后; 和 c) AnotherDependency#method1() 必须只在 DependencyAbc#method2() 调用之前。 在大多数的测试中,我们将可能只做这些不同种类顺序相关验证中的一个。 但是使得包含所有种类的复杂验证是相当容易的。

其他的上面例子没有包含的是我们需要验证确定的调用以一个给定的相对顺序, 然而需要验证其他的调用 (以任何顺序、)。针对这种情况,我们需要编写两个独立的验证块,如下面所示的 (这里模拟的是测试类中的属性)。

@Test
public void verifyFirstAndLastCallsWithOthersInBetweenInAnyOrder()
{
   // Invocations that occur while exercising the code under test:
   mock.prepare();
   mock.setSomethingElse("anotherValue");
   mock.setSomething(123);
   mock.notifyBeforeSave();
   mock.save();

   new VerificationsInOrder() {{
      mock.prepare(); // first expected call
      unverifiedInvocations(); // others at this point
      mock.notifyBeforeSave(); // just before last
      mock.save(); times = 1; // last expected call
   }};

   // Unordered verification of the invocations previously left unverified.
   // Could be ordered, but then it would be simpler to just include these invocations
   // in the previous block, at the place where "unverifiedInvocations()" is called.
   new Verifications() {{
      mock.setSomething(123);
      mock.setSomethingElse(anyString);
   }};
}

通常,当一个测试包含多个验证块,它们的相对执行顺序是重要的。在之前的测试中,如果一个无序的块出现在它之前将会不会留下 "unverified invocations" 进行之后的对 unverifiedInvocations()的匹配; 这个测试也会通过 (加入它原来是能通过的) 因为它不需要一个非验证的调用发生在调用位置之前, 但是它不会验证在首个和最后一个其他调用之间的未排序的调用组。

全验证

有时可能包含在测试中的所有的参与的模拟类型中的调用被验证。这可以通过使用严格的期望来自动实现,由于任何未被期待的调用都将导致测试失败。然而当在正常的期望中可以显式的验证,通过使用 "new FullVerifications() {...}" 块来确保没有调用没被验证。

@Test
public void verifyAllInvocations(@Mocked final Dependency mock)
{
   // Code under test included here for easy reference:
   mock.setSomething(123);
   mock.setSomethingElse("anotherValue");
   mock.setSomething(45);
   mock.save();

   new FullVerifications() {{
      // Verifications here are unordered, so the following invocations could be in any order.
      mock.setSomething(anyInt); // verifies two actual invocations
      mock.setSomethingElse(anyString);
      mock.save(); // if this verification (or any other above) is removed the test will fail
   }};
}

注意当一个最低限制 (一个最少的调用次数约束) 被在期望中定义时, 这个约束将会隐式的被在测试结束之后验证。因此显式的验证这个期望在全验证的块中是不需要的。

按顺序进行全验证

我们现在已经知道如何使用Verifications 不按顺序验证,使用 VerificationsInOrder 进行按顺序的验证, 和使用 FullVerifications进行全验证。但是如何进行全顺序验证 ? 也是很容易的:

@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();
   }};
}

注意尽管这里没有明显的语法上的区别。在 以上的verifyAllInvocations 测试中, 我们能够匹配两个独立的 mock.setSomething(...) 使用使用独立的在验证块中的调用。在 verifyAllInvocationsInOrder 测试中, 然而我们需要在块中编写两个独立的调用方法, 按照他们各自独立的调用顺序。

限制模拟类型集合进行全验证

缺省的,在一个给定测试中的所有的对模拟实例/类型的调用必须显式的被验证当使用 "new FullVerifications() {}" 或 "new FullVerificationsInOrder() {}" 块的时候。现在,假如我们有一个测试包含了两个模拟类型 (或更多) 但是我们只想对他们中的一个进行调用全验证 (或者对任何模拟类型的部分不只有两个)? 可以使用 FullVerifications(mockedTypesAndInstancesToVerify) 构造方法, 当只有一个给定的模拟实例和模拟类型需要被使用(例如, 类对象/名称)。如下测试提供了一个例子。

@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;
   }};
}

在如上的测试中,mock2.doSomething() 调用从来没有被验证。

为了只是对一个独立的模拟类中的方法/构造方法进行验证, 传递类名到 FullVerifications(...) 或 FullVerificationsInOrder(...) 构造方法中。 例如, 新的 FullVerificationsInOrder(AnotherDependency.class) { ... } 块只会使得所有对模拟的 AnotherDependency 类的调用被验证。

验证没有调用发生

为了验证在测试中没有调用发生在模拟的类型/实例上,可以添加一个空的全验证块到其中。 通常的,注意所有的通过一个 times/minTimes约束的录制期望都将被隐式的验证,因此不管验证块中的全验证; 在这个例子中空的验证块会验证没有其他的任何调用发生。此外,在相同的测试中之前的验证块一斤验证了期望,他们也将被全验证块所忽视。

如果测试使用了两个或者更多的模拟类型/实例,你需要验证在他们之中没有调用发生,定义所需的模拟类型和/或实例到构造方法中在空的验证块中。一个测试用例如下。

@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) {};
}

验证未定义的调用不能发生

一个全验证块(排序或者没有)允许我们验证确定的方法和/或构造方法从没有被调用,不需要在录制或验证他们的时候使用对应 times = 0 的赋值。 如下例子所示。

@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
   }};
}

如果对依赖的类的任何方法(或构造方法)的调用在重放阶段,除非这个被显式的在验证块中验证 (Dependency#getData() 在这个例子中),以上的测试将会失败。从另一方面来说, 在这个例子中更好的使用严格的期望, 完全不需要使用任何的验证块。

捕捉验证时的调用参数

调用参数可以在之后的验证中被捕捉通过一个系列的特殊 "withCapture(...)" 方法。有三种的类型, 每一个都包含他们特定的捕捉方法 : 1) 在单个调用中验证的参数是否被传入到模拟方法中: T withCapture(); 2) 验证参数是否在多个调用中被传入: T withCapture(List); 和 3) 验证参数被传入到模拟构造方法中: List withCapture(T).

从单个调用中捕捉参数

为了从单个对模拟方法或构造方法的调用捕捉参数,我们使用 "withCapture()", 正如下面的例子所展示的。

@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() 方法可以被使用在验证块中。通常我们使用它在一个单独的调用匹配是否发生; 如果有多个这个调用发生, 最后一个调用将会覆盖之前所捕捉到的值。这在处理复杂的参数类型(比如 JPA @Entity)有用, 可能包含一些项的值需要被验证。

从多个调用中捕捉参数

如果多个对模拟方法或构造方法得调用需要期待,我们需要捕捉他们中的所有值,可以使用 withCapture(List) 方法, 如下的例子所示。

@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.
   }};
}

不同于 withCapture(), withCapture(List) 重载可以在期望录制块中。 blocks.

捕捉新实例

最后,我们捕捉一个模拟类型的新创建实例在测试阶段。

@Test
public void capturingNewInstances(@Mocked Person mockedPerson)
{
   // From the code under test:
   dao.create(new Person("Paul", 10));
   dao.create(new Person("Mary", 15));
   dao.create(new Person("Joe", 20));

   new Verifications() {{
      // Captures the new instances created with a specific constructor.
      List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));

      // Now captures the instances of the same type passed to a method.
      List<Person> personsCreated = new ArrayList<>();
      dao.create(withCapture(personsCreated));

      // Finally, verifies both lists are the same.
      assertEquals(personsInstantiated, personsCreated);
   }};
}

代理:定义自己的结果

我们已经知道如何通过复值给result或使用 returns(...)方法在录制调用结果。我们也知道如何使用 withXyz(...)的一组方法和不同的 anyXyz 属性来进行灵活的调用参数匹配。但是假如一个测试需要基于它在重放阶段所得到的参数设定录制阶段的值? 我们可以使用代理实例,如下所示。

@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();
}

Delegate 接口是空的, 简单的通过代理 "delegate" 方法在赋值对象上来告诉 JMockit 在重放实际阶段上如何操作。 这个方法可以包含任何名称, 在代理对象上声明它为非私有方法。作为代理方法的参数,他们既可以是匹配录制方法的参数或他们是空的。在任何的例子中,代理方法允许包含附加的类型为Invocation的参数作为它的第一个参数。(在重放阶段所接收的 Invocation 对象将会提供调用实例和真是调用参数的方法,同样包含其他的能力。) 一个代理方法返回的类型不需要和录制方法中的一样,, 尽管它需要兼容避免之后造成 ClassCastException 异常。

构造方法可以通过代理方法进行处理。如下的例子演示了构造方法调用是如何通过一个代理的方法来有条件的情况下抛出异常。

@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);
}

级联模拟

当使用复杂的 APIs 来处理多个分布式的不同对象上的功能时,很少看到使用链式调用以 obj1.getObj2(...).getYetAnotherObj().doSomething(...)的格式。 在这种情况下,可能需要模拟所有的对象/类在链中, 从 obj1开始的。

所有的三个模拟注释都提供这个功能。 如下的例子显示了一个级别的例子, 使用 java.net 和 java.nio APIs。

@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类上的合适方法的调用都将返回一个级联模拟的对象,在测试阶段中。激烈模拟将会允许更进一步的级联,因此一个空引用将不会从返回对象的引用的方法中获取(除了一个不合适的返回类型对象或字符串返回null,或集合类型中返回了一个非模拟的空类型。)。

除了一个可用的在模拟属性/参数(例如上面的 cascadedChannel)中的模拟实例,一个新的级联实例将会在首次的对每一个模拟方法的调用中创建。在上面的例子中 , 对相同的 InetAddress 返回类型的不同方法将会创建和返回不同的级联实例; 虽然,相同的方法将会返回相同的级联实例。

新的级联实例将会通过使用 @Injectable 语法创建,因此不会影响在测试中相同类型的其他实例。

最后,如果必要的话,值得注意的是级联实例可以被非模拟类型,被不同的模式实例,或一个无返回的替换; 为此,使用所需要返回的实例对result属性赋值的期望录制或使用null如果没有这个实例需要。

级联静态工厂方法

在一个模拟类中包含了静态工厂方法的时候级联是很有用的。在如下的测试例子中,让我们看看如何模拟 javax.faces.context.FacesContext 类从 JSF (Java EE)中。

@Test
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()方法, 由于 "jsf" 模糊实例将自动返回。

级联自返回的方法

其他场景如当我们使用流式接口在测试代码中的时候,级联也是有用的,一个建造者对象通过它的大多数方法最后返回它自身。因此,我们结束一个方法调用链来产生一些 final 对象或状态,在上面的测试例子中我们模拟了 java.lang.ProcessBuilder 类。

@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() 配置了 process 来建立, 然而 start() 最终建立了它。模拟的流程构建者对象自动返回了它的 ("pb") 从这些调用, 同时也返回了一个新的模拟 Process从调用 start()之后。

部分模拟

缺省的,所有的方法和构造方法被调用在模拟类型中的和它的父类 (除了 java.lang.Object) 都被模拟了。这对于大多数的测试用例是适用的,但是在一些情况下,我们需要选择其中的一些确定的方法或构造方法来进行模拟。方法/构造方法在模拟类中没有被模拟将按照普通的调用 方式执行。

当一个类或对象被部分模拟,JMockit 决定是否执行一个方法或一个构造方法的真是实现在它的测试代码中,基于哪些在期望中被录制,哪些没有。接下来的例子将展示这个用法。

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());
   }
}

如上所示, Expectations(Object...) 构造方法接收一个或者是多个类或对象来进行部分模拟。如果给定一个类对象,在该类中的所有的方法和构造方法包括其父类中的会被模拟; 所定义的类中的所有实例将被模拟。从另外一方面来说,如果给定一个普通实例,只有方法而不是构造方法在类层次中被模拟; 甚至,只有特定的实例将会被模拟。

注意在两个测试例子中没有包含模拟的属性或模拟参数。部分模拟构造方法对于提供另外一种方式来模拟类是有用的。它也允许我们能够转变本地存储的对象实例为模拟实例。这些对象可以在内置的实例属性中建立任意数量的状态;他们将会保持状态被模拟。

也应该注意到,我们需要一个类或实例被部分模拟,也可以包含调用验证在其中,即使验证方法/构造方法没有被录制。例如考虑如下的例子。

@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); }};
}

最后,一个简单的方法来实现部分模式在一个测试类上是在测试类中标记一个测试同时为 @Tested (看如下的章节) 和 @Mocked。在这个例子中,测试类是不需要被传递到期望构造方法中的,但是我们还是需要录制任何方法的所需模拟结果在期望中。

捕捉实现类和实例

在讨论这个特征的时候我们基于如下的代码(特意制作的)。

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(), 使用了实现一个独立接口的类, Service。一个这些实现的通过一个在匿名的内部类定义,而这些通过客户端代码是不可见的(除了使用反射)。

模拟未明确实现的类

给定一个基类 (可能是一个接口,一个抽象类,或任何类型的基类 ), 我们可以编写测试只需知道这个需要被模拟基类而不是所有的实现/继承实现类。为了这样做,我们声明了一个 "捕捉的" 模拟类型引用了只知道的基类型。不仅仅已经被 JVM 载入的实现类可以被模拟,也包括一些其他的类在之后测试执行中才被 JVM载入的。 这个能力可以通过 @Capturing 注释激活, 可以用于模拟的属性和模拟参数,如下所示。

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);
   }
}

在如上的测试中,两个返回值被定义对 Service#doSomething() 方法。这个期望将会匹配所有对这个方法的调用, 不管实际实例在什么时候调用,也不管实际的类是如何实现这个方法。

future 实例的定义行为

一个其他能力关于抓捕在 future 实例上是可赋值给模拟类型, 通过使用 "maxInstances" 可选的属性来实现。 这个属性定义一个整数值来定义模拟类型的future实例将被相关的模拟属性/参数所覆盖的最大次数; 当没有明确定义, 所有的可赋值实例,包括之前存在还是在测试中被建立的豆浆被覆盖。

在一个给定捕捉的模拟属性或参数上进行期望的录制或录制将匹配在任何future实例上的通过模拟属性/参数的调用覆盖。这允许我们录制和/或验证不同的行为针对每一个future实例集合;为了这个,我们需要声明两个或者更多的相同类型的捕捉模拟属性/参数,每一个都包含自己的最大实例数目(可能要除了最后一个模拟属性/参数,可能会覆盖余下的future实例)。

为了展示这个功能,如下的例子使用了 java.nio.Buffer 子类和他们的 future 实例;在真实的例子中最好是使用真实的 buffers rather 而不是模拟的。

@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());
}

应该注意到当在一个范围内捕捉模拟类型,所有的实现类都将被模拟,不管任何 maxInstances 约束的定义。

实例化和注入测试对象

通常一个测试类将会使用一个独立的测试类。 JMockit 可以帮助自动实例化这个类,和可选的注入相关的模拟依赖。这也是 @Tested 注释使用的地方。

一个非final的实例属性在测试类中被注释将会被认为是一个自动初始化和注入,在一个测试方法被执行之前。如果这个时候这个属性还是为null 引用, 一个实例将会通过使用在测试类中的恰当的构造方法建立, 但是需要确保其内置的依赖能够被切当的注入 (当使用的时候)。如果这个属性已经被初始化 (不是 null), 则不会有任何事情发生。

为了能够注入 ,测试类中必须包含一个或多个模拟属性或模拟参数声明为 @Injectable。 模拟属性/参数只通过 @Mocked 或 @Capturing注释的将不被认为注入。换句话来说就是,不是所有的可以注入的属性/参数都需要包含模拟类型; 他们也可以包含原始或数组类型。如下的测试类将展示。

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.
   }
}

注意一个非模拟类型的注入属性/参数都必须显式的定义一个值,否则缺省值将被使用。在这个注入属性的例子中,值可以通过简单的赋值给属性。此外,它可以提供一个 "value" 属性在 @Injectable中, 这是唯一的方式在可注入的测试方法参数中定义值。

两种注入类型能够被支持: 构造注入和属性注入。在第一个例子中,测试类中必须包含一个构造方法能够满足注入。 注意在一个给定的测试中,一系列可用的注入包括声明为测试类实例属性的注入属性和在测试方法中定义的可注入参数; 因此,在相同的测试类中的不同测试可以提供不同的注入在相同的测试代码中。

一旦测试类在被选中的构造方法初始化,它的非final实例属性将被认为是注入的。针对每一个注入的属性,相同类型的注入属性将被在测试中类搜索。如果只找到了一个,它的当前值将被读取和存储到注入属性中。如果找到不只一个,注入属性名将被用来作为从相同类型的注入属性中选择。

重用期望和验证代码块

最简单的复用测试代码在 JMockit 中就是在测试类级别中声明模拟属性。正如下面的例子所示,被建立和赋值到这些属性中的对象将被使用在任意数量的测试方法中(通过 JMockit 或显示的测试代码)。

public final class LoginServiceTest
{
   @Tested LoginService service;
   @Mocked UserAccount account;

   @Before
   public void init()
   {
      new Expectations() {{ UserAccount.find("john"); result = account; minTimes = 0; }};
   }

   @Test
   public void setAccountToLoggedInWhenPasswordMatches() throws Exception
   {
      willMatchPassword(true);

      service.login("john", "password");

      new Verifications() {{ account.setLoggedIn(true); }};
   }

   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) 方法。这个方法首次尝试查找一个存在用户账号从给定的登录名称中 ("accountId", 作为所有账号中的唯一标识)。 由于一些不同的测试需要完全的使用这个方法,一个对 UserAccount#find(String accountId) 方法的调用被录制在这个测试类中的, 使用一个特殊的登录名 ("john") 和模拟账号作为返回值。 注意, 任何给定的测试都可以使用多个期望和/或验证块。这些块可以各自的写在共享的 "before" 和 "after" 方法中。

另外上面一个重用的例子通过使用 willMatchPassword(boolean) 方法来显示,其中包括了另外一个可以重用的期望块。在这个例子中,一个匹配任何密码值对 UserAccount#passwordMatches(String) 方法的调用被录制,在重用方法中通过参数提供了一个调用返回值。

另外一个重用期望和验证的方法就是建立命名类而不是匿名的。例如,可以使用一个重用的内部类来代替 the willMatchPassword(boolean) 方法的使用:

final class PasswordMatchingExpectations extends Expectations
{
   PasswordMatchingExpectations(boolean match)
   {
      account.passwordMatches(anyString); result = match;
   }
}

@Test
public void setAccountToLoggedInWhenPasswordMatches() throws Exception
{
   new PasswordMatchingExpectations(true);

   ...
}

注意这些类必须声明为final,除非他们将被作为一个基类针对进一步的扩展。例如非final的基类必须以"Expectations" 或 "Verifications"命名结束;要不然他们将不会被 JMockit识别。

最后,可重用的期望/验证子类也可以是顶级类,允许他们在任意数量的测试类中使用。

其他主题

接下来的章节将会介绍很少发生的情况。

同时模拟多个接口

假如一个测试代码需要模拟一个实现了两个甚至更多的接口的类。如下的例子展示如何实现。

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);
   }
}

在如上的测试中,两个接口在一起被模拟 : 依赖于 java.lang.Runnable 针对一个模拟属性 , 和依赖于 java.io.Serializable 针对模拟参数。我们使用类型变量 MultiMock (在整个测试代码中定义的) 和 M (定义了一个测试方法) 因此 JMockit 将指导每一个例子中的组合接口。

迭代期望

当一系列连续的调用被在严格的期望中录制 (否则在调用的相关顺序是无关的), 整个序列期待在重放阶段准备的执行一次。然而考虑到, 这个例子中测试代码将在一个循环中进行调用 (或任何类型的迭代)。假如在测试中的迭代数目是知道的,我们可以录制这些期望通过在一个循环中简单的调用每一个方法/构造方法(也就是,不需要在一个期望块中编写一个循环或重复这个期望)。接下来的测试将会展示这个特征,使用 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();
}

针对一组的调用定义迭代次数的能力也适用于普通的期望。然而这个例子中所定义的迭代次数只是作为上下限调用次数约束的乘数 (包括隐式和显式的)。

验证迭代

正如我们所看到的录制期望块,验证块也可以处理在外部循环中调用,针对一个特殊数目的循环迭代。

@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();
   }};
}

以上两个验证块只是为了解释不通的语法在按顺序和无序的迭代验证块。在第一个块中,每一个验证调用将会匹配至少三个调用相同的方法在重复阶段,因为这是构造方法中传递的迭代数目。 针对一个无序的迭代块,特定数目的迭代可以通过最小和上限与调用次数进行相乘; 即使一个显式的约束被定义在块中,例如 minTimes = 1; maxTimes = 4; 成对的赋值,在这个特殊例子中将为 minTimes = 3; maxTimes = 12;在第二个块中,在另外一方面来看, 调用次数是没有作用的。相反的,这是效果等价于循环展开,好像整个在块中的调用验证是在每个迭代中进行重复。

一个迭代 FullVerifications 块的语法和普通的 Verifications块类似。这也同样适用于迭代的 FullVerificationsInOrder 块,正如VerificationsInOrder 块。