Mockito的when()调用是如何工作的?

141

给定以下 Mockito 语句:

when(mock.method()).thenReturn(someValue);

当使用mockito中的mock对象调用mock.method()方法时,mockito是如何创建一个代理对象,并将返回值传递给when()方法的呢?我想这可能涉及到一些CGLib的东西,但我很想知道这是如何实现的。

2个回答

139
简单来说,在你的示例中,mock.method() 的结果将是一个类型相应的空值;Mockito 使用代理、方法拦截和共享的 MockingProgress 类的实例来确定对模拟对象方法的调用是用于存根化还是重播已经存在的存根化行为,而不是通过模拟方法的返回值传递有关存根信息。
在查看Mockito代码的几分钟小分析如下。注意,这只是一个非常粗略的描述——这里涉及了很多细节。建议您自己查看Github上的源代码
首先,当您使用Mockito类的mock方法进行类模拟时,实际上会发生以下情况:
1. Mockito.mock 委托给 org.mockito.internal.MockitoCore.mock,并将默认的模拟设置作为参数传递。 2. MockitoCore.mock 委托给 org.mockito.internal.util.MockUtil.createMock 3. MockUtil 类使用 ClassPathLoader 类获取 MockMaker 实例,用于创建模拟对象。默认情况下使用 CgLibMockMaker 类。 4. CgLibMockMaker 使用从 JMock 借来的一个类 ClassImposterizer 处理创建模拟对象的过程。使用的“mockito 魔法”关键部分是用于创建模拟对象的 MethodInterceptor:Mockito 的 MethodInterceptorFilter,以及一系列的 MockHandler 实例,包括一个 MockHandlerImpl 实例。方法拦截器将调用传递到 MockHandlerImpl 实例,MockHandlerImpl 实现了应在模拟对象上调用方法时应用的业务逻辑(即搜索查看是否已记录答案,确定调用是否表示新的存根等)。默认状态是,如果尚未为正在调用的方法注册存根,则返回类型相应的空值
现在,让我们看看您示例中的代码:
when(mock.method()).thenReturn(someValue)

此代码的执行顺序如下:

  1. mock.method()
  2. when(<step 1 的结果>)
  3. <step 2 的结果>.thenReturn

理解此过程的关键在于对 mock 方法被调用时发生的情况有所了解:方法拦截器将接收有关方法调用的信息,并委托给其 MockHandler 实例链,这些实例最终会委托给 MockHandlerImpl#handle. 在 MockHandlerImpl#handle 过程中,mock 处理程序创建了一个 OngoingStubbingImpl 实例,并将其传递给共享的 MockingProgress 实例。

当在调用 method() 之后再调用 when 方法时,它会委托给 MockitoCore.when,该方法会调用相同类的stub() 方法。该方法从被模拟的 method() 调用写入的共享的 MockingProgress 实例中拆分 ongoing stubbing,并返回它。然后在 OngoingStubbing 实例上调用 thenReturn 方法。


1
感谢您详细的回复。还有一个问题 - 您提到“当在 method() 调用之后调用 when() 方法时” - 它是如何知道 when() 方法的调用是紧接着 method() 方法的下一个调用(或者包装了 method() 方法的调用)?希望这样说得清楚。 - marchaos
@marchaos 它不知道。使用 when(mock.method()).thenXyz(...) 语法时,mock.method() 在“回放”模式下执行,而不是在“存根”模式下执行。通常,此执行 mock.method() 没有任何效果,因此稍后当执行 thenXyz(...) (thenReturn, thenThrow, thenAnswer 等) 时,它进入“存根”模式,然后记录该方法调用的所需结果。 - Rogério
1
Rogerio,实际上比那更微妙一些——Mockito没有显式的存根和重放模式。我稍后会编辑我的答案,使其更加清晰。 - Paul Morie
简而言之,使用CGLIB或Javassist在另一个方法中拦截方法调用比拦截“if”运算符更容易。 - Infeligo
巧妙。乍一看,我会认为 Mockito.when() 需要惰性评估其参数才能正常工作。调用 mock.method() 将被暂停并传递给 Mockito.when(),然后 Mockito.when() 需要检查挂起的操作,发现 mock 实例和 method 方法调用的名称,然后设置即将到来的 method() 期望响应的定义。JVM 需要几个功能和运行时代码内省特性,它没有这些功能。但是通过具有静态的 MockingProgress 管理元素来解决了所有问题。 - David Tonhofer
显示剩余2条评论

45
简短回答是,幕后,Mockito使用一些全局变量/存储来保存方法存根构建步骤的信息(在您的示例中调用method()、when()、thenReturn()),因此最终它可以构建一个映射,指定当参数被调用时应该返回什么。
我发现这篇文章非常有帮助:解释基于代理的模拟框架的工作原理(http://blog.rseiler.at/2014/06/explanation-how-proxy-based-mock.html)。作者实现了一个演示模拟框架,我认为这是一个非常好的资源,适合想要弄清楚这些模拟框架如何工作的人。
在我看来,这是一个典型的反面模式用法。通常在实现方法时我们应该避免'副作用',这意味着该方法应该接受输入并执行一些计算,并返回结果——除此以外不应做出任何其他更改。但是Mockito故意违反了这个规则。它的方法除了返回结果还存储了一堆信息:Mockito.anyString()、mockInstance.method()、when()、thenReturn等都有特殊的'副作用'。这也是为什么这个框架一开始看起来像魔术的原因——我们通常不这样编写代码。然而,在模拟框架的情况下,这种反面模式设计是一个很好的设计,因为它导致了非常简单的API。

5
优秀的链接。其背后的天才之处在于:非常简单易用的API。还有一个很棒的决定是,when()方法使用了泛型,使得thenReturn()方法是类型安全的。 - David Tonhofer
2
我认为这是更好的答案。与其他答案相比,它清楚地解释了模拟过程的概念,而不是通过具体代码控制流程。我同意,链接很棒。 - mihca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接