Mockito匹配器如何工作?

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

  • Mockito匹配器通常会导致InvalidUseOfMatchersExceptionexception,即使在使用任何匹配器后执行的代码也是如此。

  • Mockito匹配器会受到怪异规则的约束,例如只要在给定方法中使用匹配器的情况下,只需要使用Mockito匹配器即可。

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

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

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

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风格的匹配器分开, 因此匹配expression式的描述直接适合方法调用 : Mockito匹配器返回T ,其中Hamcrest匹配器方法返回匹配对象( Matcher<T>types)。

Mockito匹配器通过org.mockito.Matchersorg.mockito.AdditionalMatcherseqanygtstartsWith等静态方法调用。 还有适配器,这些适配器已经在Mockito版本中进行了更改:

  • 对于Mockito 1.x来说, Matchers会将一些调用(如intThatargThat )作为Mockito匹配器,直接接受Hamcrest匹配器作为参数。 ArgumentMatcher<T>扩展org.hamcrest.Matcher<T> ,用于Hamcrest内部表示,是一个Hamcrest匹配器基类,而不是任何types的Mockito匹配器。
  • 对于Mockito 2.0+,Mockito不再有对Hamcrest的直接依赖。 Matchers调用intThatintThatargThat包装不再实现org.hamcrest.Matcher<T>但以类似方式使用的ArgumentMatcher<T>对象。 Hamcrest适配器如argThatintThat仍然可用,但是已经转移到MockitoHamcrest

不pipe匹配者是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匹配器会将整个expression式包装成一个单独的调用,就像示例代码的第一行一样。

什么匹配器做/返回

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

当不使用参数匹配器时,Mockito会logging您的参数值并将其与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 

当你像any或者gt (大于)那样调用一个匹配器的时候,Mockito存储一个匹配器对象,这个匹配器对象会导致Mockito跳过这个相等检查并应用你select的匹配。 在argumentCaptor.capture()的情况下,它存储一个保存其参数的匹配器,以备后续检查。

匹配器返回虚拟值,例如零,空集合或null 。 Mockito试图返回一个安全的,合适的虚拟值,比如anyInt()或者any(Integer.class) 0或者any(Integer.class) anyListOf(String.class)一个空的List<String> 。 但是,由于types擦除,Mockito缺lesstypes信息来返回any any()argThat(...)任何值,但如果尝试“自动取消” null原始值,则会导致NullPointerException。

eqgt这样的匹配器取参数值; 理想情况下,应在计算/validation开始之前计算这些值。 在嘲笑另一个电话的过程中调用一个模拟可能会干扰stubbing。

匹配方法不能用作返回值; 例如,在thenReturn(anyInt())没有任何方法来执行thenReturn(anyInt())thenReturn(any(Foo.class)) 。 Mockito需要确切地知道哪个实例在存根调用中返回,并且不会为您select任意的返回值。

实施细节

匹配器被存储在一个名为ArgumentMatcherStorage的类中的栈中(作为Hamcrest风格的对象匹配器)。 MockitoCore和Matchers都拥有一个ThreadSafeMockingProgress实例,它静态地包含一个持有MockingProgress实例的ThreadLocal。 这是MockingProgressImpl ,它包含一个具体的ArgumentMatcherStorageImpl 。 因此,模拟和匹配器状态是静态的,但是在Mockito和Matchers类之间一致的线程范围。

大多数匹配器调用只能添加到这个堆栈中,除了匹配器例如andornot 。 这完全符合(并依赖于) 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))存根,并清除堆栈。

这演示了一些规则:

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

  • 呼叫订单不只是重要的,这是什么使这一切工作 。 提取匹配器到variables通常不起作用,因为它通常会改变呼叫顺序。 提取匹配器的方法,但是,效果很好。

     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或者模拟互动时检查堆栈,并且必须接受匹配器而不知道它们是立即使用还是意外抛弃。 从理论上讲,堆栈应该在调用when或者verify之外总是空的,但是Mockito不能自动检查。 您可以使用Mockito.validateMockitoUsage()手动检查。

  • 在调用的when ,Mockito实际上调用了有问题的方法,如果你用方法抛出一个exception(或者需要非零值或者非空值),这将抛出一个exception。 doReturndoAnswer (等) 调用实际的方法,通常是一个有用的select。

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

  • 如果你试图做一些不好的事情,比如存根/validation一个最后的方法 ,Mockito会调用真正的方法, 并且在堆栈上留下额外的匹配器final方法调用可能不会抛出一个exception,但是当你下一次与一个模拟进行交互时,你可能会从杂散匹配器中得到一个InvalidUseOfMatchersExceptionexception。

常见的问题

  • InvalidUseOfMatchersException

    • 请检查每个参数是否只有一个匹配器调用,如果您使用匹配器,并且没有使用whenverify调用以外的匹配器。 匹配器不应该被用作存根返回值或字段/variables。

    • 检查你是不是把一个模拟器作为提供匹配器参数的一部分。

    • 检查你是不是试图用匹配器来validation最终的方法。 这是将匹配器放在堆栈上的好方法,除非最终的方法抛出exception,否则这可能是唯一一次你意识到你嘲笑的方法是最终的。

  • 带有原始参数的NullPointerException: (Integer) any()返回null,而any(Integer.class)返回0; 这可能会导致一个NullPointerException如果你期待一个int而不是整数。 在任何情况下,首选anyInt() ,将返回零,也跳过自动装箱步骤。

  • NullPointerException或其他exception:调用when(foo.bar(any())).thenReturn(baz)实际上会调用 foo.bar(null) ,当您收到一个空参数时,您可能已经foo.bar(null)引发exception。 切换到doReturn(baz).when(foo).bar(any()) 跳过存根行为 。

一般故障排除

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

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

杰夫·鲍曼的杰出答案只是一个小的补充,因为当我寻找解决scheme来解决我自己的一个问题时,我发现了这个问题:

如果一个方法的调用when经过训练的呼叫when匹配多个模拟,那么调用的时间顺序是重要的,应该是从最宽到最具体。 从Jeff的一个例子开始:

 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 

如果你反过来,那么结果将永远是true