Mockito匹配器如何工作?


122

争论的Mockito匹配器(如anyargThateqsame,和ArgumentCaptor.capture())从Hamcrest匹配器表现非常不同。

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

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

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

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

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

Answers:


236

匹配器的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匹配器返回THamcrest匹配器方法返回Matcher对象(类型Matcher<T>)的地方。

匹配器的Mockito通过静态方法,如调用eqanygt,和startsWithorg.mockito.Matchersorg.mockito.AdditionalMatchers。还有一些适配器,这些适配器在Mockito版本之间进行了更改:

  • 对于Mockito 1.x,Matchers一些调用(例如intThatargThat)是Mockito匹配器,它们直接接受Hamcrest匹配器作为参数。ArgumentMatcher<T>extended org.hamcrest.Matcher<T>,用于内部Hamcrest表示形式,是Hamcrest匹配器的基类,而不是任何Mockito匹配器。
  • 对于Mockito 2.0 +,Mockito不再直接依赖Hamcrest。Matchers调用短语为intThatargThat包装ArgumentMatcher<T>不再实现org.hamcrest.Matcher<T>但以相似方式使用的对象。Hamcrest适配器(例如argThat和)intThat仍然可用,但已MockitoHamcrest改为使用。

无论匹配器是Hamcrest还是简单的Hamcrest风格,都可以像这样进行修改:

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

在上面的语句中:foo.setPowerLevel是一个接受的方法intis(greaterThan(9000))返回一个Matcher<Integer>,它不会作为setPowerLevel参数。Mockito匹配器intThat包装了Hamcrest风格的Matcher并返回一个,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返回一个安全,适当的虚值,如0 anyInt()any(Integer.class)或空List<String>anyListOf(String.class)。但是,由于类型擦除,Mockito缺少用于返回任何值的类型信息,但nullfor any()argThat(...)则可能导致NullPointerException,如果尝试“自动拆箱” null原始值。

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

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

实施细节

匹配器(作为Hamcrest样式的对象匹配器)存储在称为ArgumentMatcherStorage的类中的堆栈中。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. Call 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.
  • 堆栈经常变化,以至于Mockito无法非常谨慎地管理它。它只能在与Mockito或模拟游戏进行交互时检查堆栈,并且必须接受匹配器而不知道它们是立即使用还是被意外丢弃。从理论上讲,在调用when或之外,堆栈应始终为空verify,但Mockito不能自动检查。您可以使用手动检查Mockito.validateMockitoUsage()

  • 在对的调用中when,Mockito实际上会调用有问题的方法,如果您对方法进行了更改以引发异常(或要求非零或非空值),则该方法将引发异常。 doReturndoAnswer(etc)不会调用实际方法,通常是一个有用的选择。

  • 如果您在存根过程中调用了模拟方法(例如,为 eq匹配器),则Mockito会根据调用检查堆栈长度,并且可能会失败。

  • 如果您尝试做一些不好的事情,例如对最终方法进行存根/验证,则Mockito将调用real方法,并在堆栈上保留额外的匹配器。的final方法调用可能不会抛出异常,但你可能会得到一个InvalidUseOfMatchersException从流浪的匹配,当你下一次交互使用模拟。

常见问题

  • InvalidUseOfMatchersException

    • 如果您根本不使用匹配器,请检查每个参数是否恰好有一个匹配器调用,并且没有在 when or verify调用调用。绝不能将匹配器用作存根返回值或字段/变量。

    • 检查是否没有在提供匹配器参数的过程中调用模拟程序。

    • 检查您是否没有尝试使用匹配器对最终方法进行存根/验证。这是在堆栈上保留匹配器的好方法,除非您的final方法抛出异常,否则这可能是您唯一一次意识到所模拟的方法是final方法。

  • 具有原始参数的NullPointerException: (Integer) any()返回null,而any(Integer.class)返回0;NullPointerException如果您期望int使用Integer而不是Integer,则会导致此错误。无论如何,还是喜欢anyInt()都将返回零,并跳过自动装箱步骤。

  • NullPointerException或其他异常:对的调用when(foo.bar(any())).thenReturn(baz)将实际调用 foo.bar(null),您可能已经过桩,在收到null参数时会抛出异常。切换到doReturn(baz).when(foo).bar(any()) 跳过存根行为

常规故障排除

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

  • 为了进行调试,validateMockitoUsage请直接在代码中添加对的调用。如果堆栈中有任何东西,则会抛出该错误,这是对不良症状的一个很好的警告。


2
谢谢你写这篇文章。带有when / thenReturn格式的NullPointerException导致我遇到问题,直到将其更改为doReturn / when。
yngwietiger 2015年

11

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

如果对某个方法的调用与多个模拟的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

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


2
尽管这是有用的信息,但它是关于存根而不是匹配项的,因此在这个问题上可能没有任何意义。顺序确实很重要,但是只有在最后定义的匹配链获胜的情况下:这意味着通常将共存存根声明为最不特定于特定存根,但是在某些情况下,您可能希望在单个测试用例中广泛覆盖特定模拟的行为。 ,这时可能需要最后给出一个广义的定义。
杰夫·鲍曼

1
@JeffBowman我认为在这个问题上是有道理的,因为该问题与嘲笑匹配器有关,并且在存根时可以使用匹配器(就像您的大多数示例一样)。自从搜索google到解释后,我就知道了这个问题,我认为在此处提供此信息很有用。
tibtof15年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.