Mockito匹配器如何工作?

时间:2014-04-02 20:37:17

标签: java mocking mockito

Mockito参数匹配器(例如anyargThateqsameArgumentCaptor.capture())与Hamcrest匹配器的行为完全不同。

  • Mockito匹配器经常导致InvalidUseOfMatchersException,即使在使用任何匹配器后执行很久的代码中也是如此。

  • Mockito匹配器会受到奇怪的规则的影响,例如,如果给定方法中的一个参数使用匹配器,则只需要对所有参数使用Mockito匹配器。

  • 当覆盖Answer或使用(Integer) any()等时,Mockito匹配器可能会导致NullPointerException。

  • 以某种方式使用Mockito匹配器重构代码会产生异常和意外行为,并且可能会完全失败。

为什么Mockito匹配器设计得像这样,它们是如何实现的?

2 个答案:

答案 0 :(得分:214)

Mockito matchers是静态方法,并且在调用whenverify期间调用代表参数的那些方法。

Hamcrest matchers(归档版本)(或Hamcrest风格的匹配器)是无状态的通用对象实例,它们实现Matcher<T>并公开方法matches(T),如果对象匹配则返回true匹配者的标准。它们旨在避免副作用,通常用于下面的断言中。

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Mockito匹配器存在,与Hamcrest风格的匹配器分开,以便匹配表达式的描述直接适合方法调用Mockito matchers return T where Hamcrest matcher methods return Matcher objects (of type Matcher<T>).

通过eqany上的gtstartsWithorg.mockito.Matchersorg.mockito.AdditionalMatchers等静态方法调用Mockito匹配器。还有适配器,它们在Mockito版本中发生了变化:

  • 对于Mockito 1.x,Matchers特色调用(例如intThatargThat)是直接接受Hamcrest匹配器作为参数的Mockito匹配器。 ArgumentMatcher<T>扩展org.hamcrest.Matcher<T>,用于内部Hamcrest表示,并且是 Hamcrest匹配器基类,而不是任何类型的Mockito匹配器。
  • 对于Mockito 2.0+,Mockito不再直接依赖于Hamcrest。 Matchers调用intThatargThat包裹ArgumentMatcher<T>对象,这些对象不再实现org.hamcrest.Matcher<T>但以相似的方式使用。诸如argThatintThat之类的Hamcrest适配器仍然可用,但已改为MockitoHamcrest

无论匹配器是Hamcrest还是Hamcrest风格,它们都可以这样调整:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

在上面的陈述中:foo.setPowerLevel是一种接受int的方法。 is(greaterThan(9000))会返回Matcher<Integer>,它不会作为setPowerLevel参数工作。 Mockito匹配器intThat包裹Hamcrest风格的匹配器并返回int,以便可以作为参数出现;像gt(9000)这样的Mockito匹配器会将整个表达式包装成单个调用,如示例代码的第一行所示。

匹配者做什么/返回

when(foo.quux(3, 5)).thenReturn(true);

当不使用参数匹配器时,Mockito会记录您的参数值并将它们与equals方法进行比较。

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

当您拨打anygt(大于)的匹配器时,Mockito会存储一个匹配器对象,该对象会导致Mockito跳过该等等检查并应用您选择的匹配。在argumentCaptor.capture()的情况下,它存储一个匹配器,用于保存其参数,以供以后检查。

匹配器返回dummy values,例如零,空集合或null。 Mockito尝试返回一个安全,合适的虚拟值,例如anyInt()any(Integer.class)为0或List<String>为空anyListOf(String.class)。但是,由于类型擦除,Mockito缺少类型信息以返回null any()argThat(...)的任何值,如果尝试&#34; auto-unbox&#,则会导致NullPointerException 34; null原始值。

eqgt之类的匹配器会获取参数值;理想情况下,应在存根/验证开始之前计算这些值。在模拟另一个调用的过程中调用模拟可能会干扰存根。

匹配方法不能用作返回值;例如,没有办法在Mockito中发表短语thenReturn(anyInt())thenReturn(any(Foo.class))。 Mockito需要准确知道在存根调用中返回哪个实例,并且不会为您选择任意返回值。

实施细节

匹配器存储(作为Hamcrest样式的对象匹配器)包含在名为ArgumentMatcherStorage的类中的堆栈中。 MockitoCore和Matchers各自拥有一个ThreadSafeMockingProgress实例,其中静态包含一个持有MockingProgress实例的ThreadLocal。这个MockingProgressImpl具有一个具体的ArgumentMatcherStorageImpl。因此,mock和matcher状态是静态的,但在Mockito和Matchers类之间是一致的。

大多数匹配器调用仅添加到此堆栈,但and, or, and not等匹配器除外。这完全对应于(并依赖于)evaluation order of Java,它在调用方法之前从左到右计算参数:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

这将:

  1. anyInt()添加到堆栈。
  2. gt(10)添加到堆栈。
  3. lt(20)添加到堆栈。
  4. 移除gt(10)lt(20)并添加and(gt(10), lt(20))
  5. 调用foo.quux(0, 0),除非另有说明,否则返回默认值false。内部Mockito将quux(int, int)标记为最近的电话。
  6. 调用when(false),它会丢弃其参数并准备在5中标识的存根​​方法quux(int, int)。只有两个有效状态是堆栈长度为0(相等)或2(匹配器),并且堆栈上的两个匹配器(步骤1和4),因此Mockito使用any()匹配器为其第一个参数存根,并为其第二个参数存储and(gt(10), lt(20))并清除堆栈。
  7. 这表明了一些规则:

    • Mockito无法区分quux(anyInt(), 0)quux(0, anyInt())之间的区别。它们看起来像是对quux(0, 0)的调用,堆栈上有一个int匹配器。因此,如果您使用一个匹配器,则必须匹配所有参数。

    • 致电订单并不重要,它是使这一切正常工作的原因。将匹配器提取到变量通常不起作用,因为它通常会更改调用顺序。然而,将匹配器提取到方法中效果很好。

      int between10And20 = and(gt(10), lt(20));
      /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
      // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
      
      public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
      /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
      // The helper method calls the matcher methods in the right order.
      
    • 筹码经常变化,以至于Mockito不能非常谨慎地进行警告。它只能在您与Mockito或模拟器交互时检查堆栈,并且必须接受匹配器,而不知道它们是否被立即使用或意外丢弃。理论上,在调用whenverify之外,堆栈应始终为空,但Mockito无法自动检查。 您可以使用Mockito.validateMockitoUsage()手动核对。

    • 在对when的调用中,Mockito实际上调用了有问题的方法,如果您将该方法存根为异常(或要求非零或非异常),则会引发异常 - 空值)。 doReturndoAnswer(等)调用实际方法,通常是一种有用的替代方法。

    • 如果你在存根中调用了一个模拟方法(例如计算eq匹配器的答案),Mockito将根据 调用来检查堆栈长度相反,可能会失败。

    • 如果你尝试做一些不好的事情,比如stubbing/verifying a final method,Mockito会调用真正的方法,并在堆栈上留下额外的匹配器final方法调用可能不会抛出异常,但是当您下次与模拟交互时,您可能会从迷路匹配器中获得InvalidUseOfMatchersException

    常见问题

    • <强> InvalidUseOfMatchersException

      • 检查每个参数是否只有一个匹配器调用,如果您完全使用匹配器,并且您还没有在whenverify调用之外使用匹配器。不应将匹配器用作存根返回值或字段/变量。

      • 检查您是否未将模拟调用作为提供匹配器参数的一部分。

      • 检查您是否尝试使用匹配器存根/验证最终方法。这是将匹配器留在堆栈上的好方法,除非你的最终方法抛出异常,否则这可能是你唯一一次意识到你嘲笑的方法是最终的。

    • 带有基本参数的NullPointerException: (Integer) any()返回null,而any(Integer.class)返回0;如果您期望NullPointerException而不是整数,则可能会导致int。在任何情况下,首选anyInt(),它将返回零,并跳过自动装箱步骤。

    • NullPointerException或其他例外:when(foo.bar(any())).thenReturn(baz)的调用实际上会调用 foo.bar(null),您可能已将其存根以引发异常收到空参数时。切换为doReturn(baz).when(foo).bar(any()) skips the stubbed behavior

    常规故障排除

    • 使用MockitoJUnitRunner,或在tearDown@After方法中明确调用validateMockitoUsage(跑步者会自动为您执行此操作)。这有助于确定您是否滥用了匹配器。

    • 出于调试目的,请直接在代码中添加对validateMockitoUsage的调用。如果你在堆栈上有任何东西,这将抛出,这是对一个不良症状的良好警告。

答案 1 :(得分:8)

只是Jeff Bowman的一个很好的补充,我在寻找解决我自己问题的方法时发现了这个问题:

如果对方法的调用与多个模拟whenwhen次调用的调用相匹配,则when(foo.quux(anyInt(), anyInt())).thenReturn(true); when(foo.quux(anyInt(), eq(5))).thenReturn(false); 调用的顺序非常重要,应该从最广泛到最具体的。从Jeff的一个例子开始:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

是确保(可能)期望结果的顺序:

true

如果您反转when调用,则结果将始终为xmllint --noout CI_PRD_TESTING.XML