Mockito匹配器如何工作?

2022-08-31 08:03:28

Mockito 参数匹配器(如 、 、 、 和 ) 的行为与 Hamcrest 匹配器非常不同。anyargThateqsameArgumentCaptor.capture()

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

  • Mockito匹配器受制于奇怪的规则,例如,如果给定方法中的一个参数使用匹配器,则仅要求对所有参数使用Mockito匹配器。

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

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

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


答案 1

Mockito 匹配器是静态方法和对这些方法的调用,它们在调用 和 期间代表参数whenverify

Hamcrest 匹配器(存档版本)(或 Hamcrest 样式的匹配器)是无状态的通用对象实例,它们实现并公开一个方法,如果对象与 Matcher 的条件匹配,则该方法将返回 true。它们旨在消除副作用,通常用于如下断言。Matcher<T>matches(T)

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

Mockito匹配器存在,与Hamcrest风格的匹配器分开,因此匹配表达式的描述直接适合方法调用Mockito匹配器返回T,而Hamcrest匹配器方法返回Matcher对象(类型为Matcher<T>)。

Mockito 匹配器通过静态方法调用,例如 、、、、on 和 。还有一些适配器,它们在Mockito版本中发生了变化:eqanygtstartsWithorg.mockito.Matchersorg.mockito.AdditionalMatchers

  • 对于 Mockito 1.x,特色一些调用(如 or )是 Mockito 匹配器,它们直接接受 Hamcrest 匹配器作为参数。ArgumentMatcher<T>扩展的,它被用于内部的Hamcrest表示,并且是Hamcrest匹配器基类,而不是任何类型的Mockito匹配器。MatchersintThatargThatorg.hamcrest.Matcher<T>
  • 对于Mockito 2.0+,Mockito不再直接依赖Hamcrest。 调用短语为或包装 ArgumentMatcher<T>不再实现但以类似方式使用的对象。诸如 和 的 Hamcrest 适配器仍然可用,但已移至 MockitoHamcrestMatchersintThatargThatorg.hamcrest.Matcher<T>argThatintThat

无论匹配者是Hamcrest还是简单的Hamcrest风格,他们都可以像这样适应:

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

在上面的语句中:是一个接受 . 返回 一个 ,它不能用作参数。Mockito匹配器包装该Hamcrest风格的匹配器并返回一个,以便它可以显示为参数;Mockito匹配器喜欢将整个表达式包装到单个调用中,就像示例代码的第一行一样。foo.setPowerLevelintis(greaterThan(9000))Matcher<Integer>setPowerLevelintThatintgt(9000)

匹配器做什么/返回什么

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

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

匹配器返回虚拟值,如零、空集合或 。Mockito 尝试返回一个安全、适当的虚拟值,例如 0 for or 或 的空值。但是,由于类型擦除,Mockito 缺少类型信息来返回除 for 或 以外的任何值,如果尝试“自动取消装箱”基元值,这可能会导致 NullPointerException。nullanyInt()any(Integer.class)List<String>anyListOf(String.class)nullany()argThat(...)null

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

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

实现细节

匹配器(作为 Hamcrest 样式的对象匹配器)存储在名为 ArgumentMatcherStorage 的类中包含的堆栈中。MockitoCore和Matchers各自拥有一个ThreadSafeMockingProgress实例,该实例静态包含一个ShakeLocal保存MockingProgress实例。正是这个 MockingProgressImpl 包含了一个具体的 ArgumentMatcherStorageImpl。因此,mock 和 matcher 状态是静态的,但在 Mockito 和 Matchers 类之间一致地处于线程范围。

大多数匹配器调用仅添加到此堆栈中,但像 andornot 这样的匹配器除外。这完全对应于(并依赖于)Java的评估顺序,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. 调用 ,该调用(除非另有存根)返回默认值 。在内部,Mockito标记为最近的呼叫。foo.quux(0, 0)falsequux(int, int)
  6. 调用 ,它放弃其参数并准备在 5 中标识的存根方法。仅有的两个有效状态是堆栈长度为 0(相等)或 2(匹配器),并且堆栈上有两个匹配器(步骤 1 和 4),因此 Mockito 使用匹配器为其第一个参数和第二个参数存根该方法,并清除堆栈。when(false)quux(int, int)any()and(gt(10), lt(20))

这演示了一些规则:

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

  • 呼叫顺序不仅重要,而且是使这一切工作的原因。将匹配器提取到变量通常不起作用,因为它通常会更改调用顺序。但是,将匹配器提取到方法中效果很好。

    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或模拟进行交互时检查堆栈,并且必须接受匹配器,而不知道它们是立即使用还是意外放弃。从理论上讲,在调用 或 之外,堆栈应始终为空,但 Mockito 无法自动检查。您可以使用 手动检查。whenverifyMockito.validateMockitoUsage()

  • 在对 的调用中,Mockito 实际上调用了有问题的方法,如果您已将该方法存根以引发异常(或需要非零或非空值),则会引发异常。 和(etc)调用实际方法,通常是一个有用的替代方案。whendoReturndoAnswer

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

  • 如果你试图做一些不好的事情,比如存根/验证最终方法,Mockito会调用真正的方法,并在堆栈上留下额外的匹配器。方法调用可能不会引发异常,但当您下次与模拟交互时,您可能会从杂散匹配器获得 InvalidUseOfMatchersExceptionfinal

常见问题

  • InvalidUseOfMatchersException

    • 检查每个参数是否只有一个匹配器调用(如果使用匹配器),以及是否在 or 调用之外未使用匹配器。匹配器永远不应用作存根返回值或字段/变量。whenverify

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

    • 检查您是否没有尝试使用匹配器存根/验证最终方法。这是将匹配器留在堆栈上的好方法,除非您的最终方法引发异常,否则这可能是您唯一意识到您正在模拟的方法是最终方法的唯一时间。

  • 带有原始参数的 NullPointerException:返回 null,同时返回 0;这可能会导致 一个,如果你期望一个而不是一个整数。无论如何,首选 ,这将返回零,并且还跳过自动装箱步骤。(Integer) any()any(Integer.class)NullPointerExceptionintanyInt()

  • NullPointerException 或其他异常:对 will 的调用实际上调用 ,在接收空参数时,您可能已经存根以引发异常。切换到跳过存根行为when(foo.bar(any())).thenReturn(baz)foo.bar(null)doReturn(baz).when(foo).bar(any())

常规故障排除

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

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


答案 2

这只是杰夫·鲍曼(Jeff Bowman)出色答案的一个小补充,因为我在寻找解决我自己的问题之一时发现了这个问题:

如果对方法的调用与多个模拟的训练调用匹配,则调用的顺序很重要,并且应该从最宽到最具体。从杰夫的一个例子开始:whenwhen

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

是确保(可能)所需结果的顺序:

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

如果反转 when 调用,则结果将始终为 。true


推荐