Mockito匹配器如何工作?
Mockito参数匹配器(例如, argThat
, eq
, same
和ArgumentCaptor.capture()
)与Hamcrest匹配器的行为非常不同。
-
Mockito匹配器通常会导致InvalidUseOfMatchersExceptionexception,即使在使用任何匹配器后执行的代码也是如此。
-
Mockito匹配器会受到怪异规则的约束,例如只要在给定方法中使用匹配器的情况下,只需要使用Mockito匹配器即可。
-
Mockito匹配器在覆盖
Answer
或使用(Integer) any()
等时可能导致NullPointerException -
用Mockito匹配器以某种方式重构代码可能会产生exception和意外的行为,并可能完全失败。
为什么Mockito匹配器是这样devise的,它们是如何实现的?
Mockito匹配器是静态方法,并调用这些方法,这些方法在调用when
和verify
期间代表参数 。
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.Matchers
和org.mockito.AdditionalMatchers
等eq
, any
, gt
和startsWith
等静态方法调用。 还有适配器,这些适配器已经在Mockito版本中进行了更改:
- 对于Mockito 1.x来说,
Matchers
会将一些调用(如intThat
或argThat
)作为Mockito匹配器,直接接受Hamcrest匹配器作为参数。ArgumentMatcher<T>
扩展org.hamcrest.Matcher<T>
,用于Hamcrest内部表示,是一个Hamcrest匹配器基类,而不是任何types的Mockito匹配器。 - 对于Mockito 2.0+,Mockito不再有对Hamcrest的直接依赖。
Matchers
调用intThat
为intThat
或argThat
包装不再实现org.hamcrest.Matcher<T>
但以类似方式使用的ArgumentMatcher<T>
对象。 Hamcrest适配器如argThat
和intThat
仍然可用,但是已经转移到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。
像eq
和gt
这样的匹配器取参数值; 理想情况下,应在计算/validation开始之前计算这些值。 在嘲笑另一个电话的过程中调用一个模拟可能会干扰stubbing。
匹配方法不能用作返回值; 例如,在thenReturn(anyInt())
没有任何方法来执行thenReturn(anyInt())
或thenReturn(any(Foo.class))
。 Mockito需要确切地知道哪个实例在存根调用中返回,并且不会为您select任意的返回值。
实施细节
匹配器被存储在一个名为ArgumentMatcherStorage的类中的栈中(作为Hamcrest风格的对象匹配器)。 MockitoCore和Matchers都拥有一个ThreadSafeMockingProgress实例,它静态地包含一个持有MockingProgress实例的ThreadLocal。 这是MockingProgressImpl ,它包含一个具体的ArgumentMatcherStorageImpl 。 因此,模拟和匹配器状态是静态的,但是在Mockito和Matchers类之间一致的线程范围。
大多数匹配器调用只能添加到这个堆栈中,除了匹配器例如and
, or
和not
。 这完全符合(并依赖于) Java的评估顺序 ,它在调用方法之前从左到右评估参数:
when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true); [6] [5] [1] [4] [2] [3]
这会:
- 将
anyInt()
添加到堆栈。 - 将
gt(10)
添加到堆栈。 - 将
lt(20)
添加到堆栈。 - 删除
gt(10)
和lt(20)
并添加and(gt(10), lt(20))
。 - 调用
foo.quux(0, 0)
,除非另有存根,否则返回默认值false
。 内部Mockito将quux(int, int)
标记为最近的调用。 - 调用
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。doReturn
和doAnswer
(等) 不调用实际的方法,通常是一个有用的select。 -
如果您在存根中间调用模拟方法(例如计算
eq
匹配器的答案),Mockito会根据该调用检查堆栈长度,并可能失败。 -
如果你试图做一些不好的事情,比如存根/validation一个最后的方法 ,Mockito会调用真正的方法, 并且在堆栈上留下额外的匹配器 。
final
方法调用可能不会抛出一个exception,但是当你下一次与一个模拟进行交互时,你可能会从杂散匹配器中得到一个InvalidUseOfMatchersExceptionexception。
常见的问题
-
InvalidUseOfMatchersException :
-
请检查每个参数是否只有一个匹配器调用,如果您使用匹配器,并且没有使用
when
或verify
调用以外的匹配器。 匹配器不应该被用作存根返回值或字段/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
。