Mockito 匹配器是如何工作的?

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

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

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

  • 当重写 Answer或使用 (Integer) any()等时,Mockito 匹配器会导致 NullPointerException。

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

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

140772 次浏览

Mockito 匹配器 是静态方法和对这些方法的调用,代理辩论在对 whenverify的调用期间调用这些方法。

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

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

Mockito 匹配器存在,与 Hamcrest 风格的匹配器分开,这样匹配表达式的描述就可以直接与方法调用相匹配: Mockito 匹配器返回 ABC0,其中 Hamcrest 匹配器方法返回 Matcher 对象(类型为 Matcher<T>)。

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

  • 对于 Mockito 1.x,Matchers的一些调用(如 intThatargThat)是 Mockito 匹配器,它们直接接受 Hamcrest 匹配器作为参数。ArgumentMatcher<T>扩展了 org.hamcrest.Matcher<T>,它用于内部 Hamcrest 表示,是 Hamcrest 匹配器基类而不是任何类型的 Mockito 匹配器。
  • 对于 Mockito 2.0 + 来说,Mockito 不再直接依赖 Hamcrest。Matchers调用表述为 intThatargThat包装 ArgumentMatcher<T>对象,这些对象不再实现 org.hamcrest.Matcher<T>,而是以类似的方式使用。Hamcrest 适配器,如 argThatintThat仍然可用,但已转移到 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()的情况下,它存储一个匹配器,该匹配器保存它的参数,以备以后检查。

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

eqgt这样的匹配器采用参数值; 理想情况下,这些值应该在存根/验证开始之前计算出来。在嘲笑另一个电话的过程中给一个模仿者打电话可能会干扰到“撞击”。

Matcher 方法不能作为返回值使用,例如,在 Mockito 没有方法短语 thenReturn(anyInt())thenReturn(any(Foo.class))。Mockito 需要准确地知道在 stub 调用中返回哪个实例,并且不会为您选择任意的返回值。

实施细节

匹配器(作为 Hamcrest 风格的对象匹配器)存储在包含在 参数匹配存储类中的堆栈中。MockitoCore 和 Matcher 各自拥有一个 ThreadSafeMockingProgress实例,其中 静止的包含一个持有 MockingProgress 实例的 ThreadLocal。就是这个 MockingProgress装着一个混凝土 ArgumentMatcherStorageImpl。因此,mock 和 matcher state 是静态的,但是 Mockito 和 matcher 类之间的线程范围是一致的。

大多数匹配器调用只添加到这个堆栈中,对于像 ABC0,ABC1,和 not这样的匹配器,则有一个例外。这完全符合(并依赖于) 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),它放弃其参数并准备存根方法 quux(int, int),该方法在5中标识。仅有的两个有效状态是堆栈长度为0(相等)或2(匹配器) ,并且堆栈上有两个匹配器(步骤1和4) ,因此 Mockito 为该方法的第一个参数使用 any()匹配器,为第二个参数使用 and(gt(10), lt(20)),并清除堆栈。

这表明了一些规则:

  • Mockito 分辨不出 quux(anyInt(), 0)quux(0, anyInt())的区别。它们看起来都像是在堆栈上有一个 int 匹配器的对 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 或者 mock 交互时检查堆栈,并且必须接受匹配器,而不知道它们是立即使用还是意外放弃。理论上,在调用 whenverify之外,堆栈应该始终是空的,但 Mockito 不能自动检查这一点。 你可以用 Mockito.validateMockitoUsage()手动检查。

  • 在对 when的调用中,Mockito 实际上调用了有问题的方法,如果您已经按住该方法以引发异常(或者需要非零或非空值) ,该方法将引发异常。 doReturndoAnswer(等等)执行 没有调用实际的方法,通常是一个有用的替代方法。

  • 如果在存根调用过程中调用了 mock 方法(例如,为 eq匹配器计算一个应答) ,Mockito 会根据 那个调用检查堆栈长度,结果可能会失败。

  • 如果您尝试做一些不好的事情,比如 确定最终方法,Mockito 将调用实际的方法 并在堆栈上留下额外的匹配器final方法调用可能不会抛出异常,但是当您下一次与 mock 交互时,可能会从偏离的匹配器获得 InvalidUseOfMatchersException

常见问题

  • 例外 :

    • 如果使用了匹配器,请检查每个参数都有一个匹配器调用,并且在 whenverify调用之外没有使用过匹配器。匹配器永远不应该用作存根返回值或字段/变量。

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

    • 检查您是否试图使用匹配器来存根/验证最终方法。这是一种将匹配器保留在堆栈上的好方法,除非您的 final 方法抛出异常,否则这可能是您唯一一次意识到您正在模仿的方法是 final。

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

  • NullPointerException 或其他异常: when(foo.bar(any())).thenReturn(baz)的调用实际上将 打电话 foo.bar(null),您可能已经在接收到 null 参数时将其撞击以抛出异常。切换到 doReturn(baz).when(foo).bar(any()) 就不会有这种行为了

一般故障排除

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

  • 为了进行调试,请直接在代码中添加对 validateMockitoUsage的调用。如果堆栈上有任何内容,这将抛出,这是对不良症状的良好警告。

这只是对 Jeff Bowman 精彩回答的一个小小补充,因为我在寻找解决我自己问题的方法时发现了这个问题:

如果对一个方法的调用与多个 mock 的 when训练调用匹配,那么 when调用的顺序就很重要,并且应该从最广泛到最具体。从杰夫的一个例子开始:

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