Mockito匹配器如何工作?

Jef*_*ica 112 java mocking mockito

争论的Mockito匹配器(如any,argThat,eq,same,和ArgumentCaptor.capture())表现非常不同,从Hamcrest匹配器.

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

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

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

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

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

Jef*_*ica 221

Mockito匹配器是静态方法,并调用那些方法,这些方法在调用和调用期间代表参数.whenverify

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

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));
Run Code Online (Sandbox Code Playgroud)

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

匹配器的Mockito通过静态方法,如调用eq,any,gt,和startsWithorg.mockito.Matchersorg.mockito.AdditionalMatchers.还有适配器,Mockito版本已经改变:

  • 对于Mockito 1.x,Matchers有些调用(例如intThat或者argThat)是Mockito匹配器,它们直接接受Hamcrest匹配器作为参数.ArgumentMatcher<T>扩展org.hamcrest.Matcher<T>,用于内部Hamcrest表示,并且是Hamcrest匹配器基类而不是任何类型的Mockito匹配器.
  • 对于Mockito 2.0+,Mockito不再直接依赖于Hamcrest.Matchers调用intThat或者argThat包装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))));
Run Code Online (Sandbox Code Playgroud)

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

什么匹配者做/返回

when(foo.quux(3, 5)).thenReturn(true);
Run Code Online (Sandbox Code Playgroud)

当不使用参数匹配器时,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
Run Code Online (Sandbox Code Playgroud)

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

匹配器返回虚拟值,例如零,空集合或null.尝试的Mockito返回一个安全,适当的虚值,如0 anyInt()any(Integer.class)或空List<String>anyListOf(String.class).因为类型擦除,不过,缺乏的Mockito类型信息返回任何值,但nullany()或者argThat(...),如果试图以"自动拆箱"一这可能会导致一个NullPointerException null原始值.

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

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

实施细节

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

最匹配的呼叫只会增加这个堆栈,与喜欢的匹配例外and,ornot.这完全对应于(并依赖于)Java评估顺序,它在调用方法之前从左到右计算参数:

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

这将:

  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()).它们看起来像是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.
    
    Run Code Online (Sandbox Code Playgroud)
  • 筹码经常变化,以至于Mockito无法非常谨慎地对其进行警告.它只能在您与Mockito或模拟器交互时检查堆栈,并且必须接受匹配器,而不知道它们是立即使用还是意外丢弃.从理论上讲,堆栈应该永远是一个电话的空外whenverify,但能的Mockito不会自动检查.您可以手动检查Mockito.validateMockitoUsage().

  • 在调用中when,Mockito实际上调用了有问题的方法,如果你已经将方法存根为异常(或者需要非零或非空值),则会引发异常. doReturndoAnswer(等)调用实际方法,通常是一个有用的替代方法.

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

  • 如果你试图做一些不好的事情,比如对最终方法进行存根/验证,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()) 跳过存根行为.

一般故障排除

  • 使用MockitoJUnitRunner,或显式调用validateMockitoUsage在你tearDown@After方法(亚军将自动为您做的).这将有助于确定您是否滥用了匹配器.

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

  • 谢谢你的这篇文章.带有when/thenReturn格式的NullPointerException导致我出现问题,直到我将其更改为doReturn/when. (2认同)

tib*_*tof 9

只是Jeff Bowman的优秀答案的一小部分,因为我在寻找解决自己问题的方法时发现了这个问题:

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

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);
Run Code Online (Sandbox Code Playgroud)

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

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false
Run Code Online (Sandbox Code Playgroud)

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

  • 虽然这是有用的信息,但它涉及 [stubbing,not matchers](http://stackoverflow.com/a/20104664/1426891),所以在这个问题上可能没有意义。顺序确实很重要,但仅限于*最后定义的匹配链获胜*:这意味着共存的存根通常被声明为最特定到最少,但在某些情况下,您可能希望在单个特定模拟行为中进行非常广泛的覆盖测试用例,此时可能需要最后一个广泛的定义。 (2认同)
  • @JeffBowman 我认为这个问题是有道理的,因为这个问题是关于模拟匹配器的,并且匹配器可以在存根时使用(就像你的大多数例子一样)。由于在谷歌上搜索解释让我想到了这个问题,我认为在这里提供这些信息很有用。 (2认同)