添加链接 注册    登录
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
开朗的打火机  ·  android ...·  9 月前    · 
八块腹肌的小蝌蚪  ·  深瞳丨高校排行“榜”架了谁?-中国科技网·  9 月前    · 
闯红灯的大熊猫  ·  Python解析xml文件: ...·  1 年前    · 
朝气蓬勃的洋葱  ·  virtualization - ...·  1 年前    · 
发财的脆皮肠  ·  Springdoc Openapi ...·  1 年前    · 
link管理  ›  Mockito 源码分析(3)——When 与 Then 与 Verify | Jitwxs
源码 mockito
https://jitwxs.cn/921b3e8a
高大的灌汤包
1 年前
avatar
文章
232
标签
135
分类
44

首页
文章
  • 分类
  • 标签
  • 时间轴
收藏夹
友链
关于
管理
  • 更新日志
  • 站点统计
Jitwxs
搜索
首页
文章
  • 分类
  • 标签
  • 时间轴
收藏夹
友链
关于
管理
  • 更新日志
  • 站点统计

Mockito 源码分析(3)——When 与 Then 与 Verify

Jitwxs | 发表于 2021-09-20 | 更新于 2021-11-13 | Java Basic Unit Test
| 字数总计: 3k | 阅读时长: 10分钟 | 阅读量:
本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue

先问大家一个问题,下图中我使用框子标出的代码,你觉得是红色、绿色、蓝色,执行的顺序是怎样的呢?

:::details 看一眼答案

执行顺序:蓝色 > 绿色 > 红色,想错了的面壁吧。。。

在开始本文之前,咱先根据前面的文章,明确一下已知条件:

  • 使用 Mockito#mock 返回的对象,它所属的类是被 mock 类的 proxy 代理类。
  • 对于该 proxy 类:
  • 是被 mock 类的子类,并且实现了 MockAccess 接口
  • 具有一个名为 mockitoInterceptor 的属性,其类型为 MockMethodInterceptor
  • 类中所有的方法都被拦截了,拦截处理的类为 DispatcherDefaultingToRealMethod
  • Matcher

    首先执行的方法是 Mockito.eq(1L) ,在 Mockito 的父类 ArgumentMatchers,为我们提供了一系列的 Match 方法, eq() 只是其中一个。

    这些方法的底层调用的都是 reportMatcher 这个方法:

    在这个方法中,有个我们的老朋友: mockingProgress() ,它在上一节中跟我们有过一面之缘,在本篇文章我们中将会频繁的碰上它。重复下它的作用: 基于 ThreadLocal 实现保存了一个 MockingProgressImpl 。

    因此 getArgumentMatcherStorage() 操作的就是当前线程 MockingProgressImpl 中的 ArgumentMatcherStorage 对象,并调用它的 reportMatcher() 方法,如下图所示。

    reportMatcher() 方法内部,其实是将 Matcher 封装成 LocalizedMatcher 对象,丢进了一个 Stack 中。

    LocalizedMatcher 的构造方法中还有一个 Location 的变量,它的作用其实是保存下来当前调用的源码路径。

    以我当前的 UT 为例,运行后它长这个样子:

    这其实是个小技巧,通过 new Throwable() 获取堆栈信息,然后从中获取有效信息,挺有意思的。

    DispatcherDefaultingToRealMethod

    执行完 Mockito.eq(1L) ,下面就开始打桩了,开头说过 OrderClient 这个类已经被 proxy 代理了,因此当 orderClient.queryById(...) 被调用时,会被 DispatcherDefaultingToRealMethod 所拦截。

    在这个类中有两个方法,至于具体执行哪一个,我暂时没找到明确的官方证明,但根据我的 Debug 和方法名称推测:

  • interceptSuperCallable:对于自身有实现的普通方法,被该方法所拦截。
  • interceptAbstract:对于抽象方法、接口类(非 default)的方法这种自身没有实现的方法,被该方法所拦截。
  • 这两个方法唯一的不同,就是当 Stub 不匹配后的容错处理逻辑。对于 interceptSuperCallable(),它可以调用自身的实现,即 callable.call() 。而 interceptAbstract(),由于自身并没有实现,所以只能抛出异常了。

    该类中用到了非常多的注解,这些注解其实都是 bytebuddy 所提供的,根据名字相信大家已经能猜出它的涵义了。

    回过头来,OrderClient 本身是个接口,因此会执行 interceptAbstract 方法,然后被交给了 MockMethodInterceptor 的doIntercept 方法处理,如下图所示:

    在该方法中,又交给了 handle 处理,这个 handle 其实就是在我们构建 proxy 时传入的那个 MockHandleImpl 对象。

    记不得的同学,复习下这几个方法:

  • org.mockito.internal.util.MockUtil#createMock
  • org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker#createMock
  • handle 方法的第一个参数是 Invocation,这个对象其实就是把本次调用的信息给整合起来了:

    MockHandleImpl

    当你对 proxy 类,无论是 when 还是 then 还是 verify 还是普通调用啥的,都会执行 MockHandlerImpl#handle 方法,因此该方法内容很长。所以我会只介绍当前相关的,其他的逻辑会暂时跳过并在下文介绍。

    在 MockHandlerImpl#handle 方法中,第一个需要关注的逻辑是如下代码:

    1
    InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(mockingProgress().getArgumentMatcherStorage(), invocation);

    这个方法的主要作用是: 将之前保存的 ArgumentMatchers 信息都取出来,然后跟 Invocation 绑定在一起,组成 InvocationMatcher 。

    第二块需要关注逻辑是如下代码:

    1
    2
    3
    4
    // prepare invocation for stubbing
    invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
    OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
    mockingProgress().reportOngoingStubbing(ongoingStubbing);

    首先 setInvocationForPotentialStubbing 将 Invocation 存入了 registeredInvocations,然后将 InvocationMatcher 放入属性 invocationForStubbing 中。

    最后再将整个 invocationContainer 封装成 OngoingStubbingImpl 放入 ThreadLocal 中。

    执行完 orderClient.queryById(...) ,下面该执行的就是 Mocktio.when(...) 了,如下图所示:

    上面代码的主要功能是:

    通过 stubbingStarted() 设置 ThreadLocal 中的 stubbingInProgress。

    这里我们可以看到设置 stubbingInProgress 前它会校验 stubbingInProgress 是否存在,因此如果你直接连续两次调用 when 就会触发这里的校验失败。

    1
    OngoingStubbing<Order> stubbing = Mockito.when(orderClient.queryById(Mockito.eq(1L)));

    这行代码执行完毕后 ,目前已知的条件有:

    When 方法执行的相关信息已经被保存到了 OngoingStubbing 中

    ThreadLocal 中目前已经设置了 stubbingInProgress,它记录了 Mockito.when(orderClient.queryById(Mockito.eq(1L))) 的源码信息

    OngoingStubbingImpl 中的 invocationContainer 中的 registeredInvocations,存储了 orderClient.queryById(...) 的调用信息

    下面开始分析 stubbing.thenReturn(...) 源码,你会发现不论你是 thenReturn 还是 thenThrow 还是 thenCallRealMethod,走的其实都是 thenAnswer 的 API。

    thenAnswer 方法代码如下,主要有三个功能:

  • 判断 registeredInvocations 中是否有数据
  • 将 answer 数据作为期望的返回值,保存起来(addAnswer)
  • 返回 ConsecutiveStubbing 对象(这个下文再细说)
  • 我们重点看 invocationContainer.addAnswer(answer, strictness) ,如下图所示,图中蓝色序号是比较关键的代码:

    ① 通过调用 removeLast 方法从 invocationContainer 中移除一个 registeredInvocations。来保证了每个 OngoingStubbing 仅能设置一次 answer。

    ② 调用重载的 addAnswer 方法时, isConsecutive 方法传递为 false,它跟 OngoingStubbing 强绑定,即 OngoingStubbing 调用时永远为 false。

    ③ 将 ThreadLocal 中的 stubbingInProgress 清除,表示之前调用 when 的数据已经不需要了。

    ④ 设置结果,这里封装了一个 StubbedInvocationMatcher 对象,它其实就代表了一个桩(Stub)。可以看到它的构造方法中已经把需要的数据都准备好了:

  • invocation.getInvocation() :对应的方法信息,即 orderClient.queryById(...)
  • invocation.getMatchers() :对应的方法参数匹配信息,即 Mockito.eq(1L)
  • answer :对应的方法返回值
  • 现在我们再把刚刚带过的 ConsecutiveStubbing 给补上,通过类图,我们发现它跟 OngoingStubbing 同级。

    二者的区别是使用 ConsecutiveStubbing 时,可以对一个 Stub,添加多个 answer,它的 thenAnswer 方法参数的 isConsecutive 就为 true 了,这也是为什么 StubbedInvocationMatcher 底层的 answers 用了 Queue 来存储的原因。

    因此,下面的写法是错误的:

    1
    2
    3
    4
    OngoingStubbing<Order> stubbing = Mockito.when(orderClient.queryById(Mockito.eq(1L)));

    stubbing.thenReturn(order1);
    stubbing.thenReturn(order2);

    因为 OngoingStubbing 的 thenAnswer 方法会调用 removeLast(),里面会清除 registeredInvocations,导致第二次执行时无法通过校验:

    正确的写法应该是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 返回子类 OngoingStubbingImpl
    OngoingStubbing<Order> stubbing = Mockito.when(orderClient.queryById(Mockito.eq(1L)));
    // 返回子类 ConsecutiveStubbing
    OngoingStubbing<Order> stubbing1 = stubbing.thenReturn(order1);
    stubbing1.thenReturn(order2);

    // 或者
    OngoingStubbing<Order> stubbing = Mockito.when(orderClient.queryById(Mockito.eq(1L)));
    stubbing.thenReturn(order1).thenReturn(order2);

    Running

    下面开始真正测试了,当执行如下代码时,期望能够从 Stub 中取出数据:

    1
    final Order order1 = orderClient.queryById(1L);

    源码还得回到 MockHandlerImpl#handle 来,没办法谁让所有方法都被它给代理了呢。

    你会发现现在走的流程跟 orderClient.queryById(Mockito.eq(1L)) 一模一样,唯一不同的是 invocationContainer.findAnswerFor(invocation) 方法返回的不再是 NULL。

    findAnswerFor() 的功能就是根据 invocation 从所有的 Stub 中寻找是否存在匹配的 Stub,如果找到匹配的,返回对应的结果。

    answer 方法中会判断 answers 的元素数量,如果有多个(ConsecutiveStubbing 情况),返回头部元素并从 Queue 中移除;如果只有一个(OngoingStubbingImpl 或仅剩一个的 ConsecutiveStubbing 情况),返回这个并且不从 Queue 中移除。

    下面让我们重点看下 findAnswerFor 是如何匹配 Stub 的,如下所示:

    可以看到匹配的条件包括:

  • 对象是否是同一个
  • 方法的名称和参数是否完全相同
  • 方法的调用参数是否完全相同
  • 这里特别要注意的是,第一个条件比较的是对象,而不是类,因此下面的 UT 是无法通过的:

    1
    2
    3
    4
    5
    6
    7
    final OrderClient orderClient1 = PowerMockito.mock(OrderClient.class);
    final OrderClient orderClient2 = PowerMockito.mock(OrderClient.class);

    PowerMockito.when(orderClient1.queryById(Mockito.eq(1L))).thenReturn(order);

    final Order actualOrder = orderClient2.queryById(1L); // null
    Assert.assertEquals(order, actualOrder);

    插个题外话,上面我说了一句:

    你会发现现在走的流程跟 orderClient.queryById(Mockito.eq(1L)) 一模一样,唯一不同的是 invocationContainer.findAnswerFor(invocation) 方法返回的不再是 NULL。

    那么你觉得下面的代码运行结果是啥?

    1
    2
    3
    4
    5
    6
    7
    8
    final OrderClient orderClient = PowerMockito.mock(OrderClient.class);

    PowerMockito.when(orderClient.queryById(Mockito.eq(1L))).thenReturn(order);

    final Order order1 = orderClient.queryById(1L);
    final Order order2 = orderClient.queryById(Mockito.eq(1L));

    Assert.assertEquals(order1, order2);

    一开始我认为这走的逻辑都是一样的啊,上面的代码执行肯定是相等的,其实运行结果是不相等。原因就在于 Mockito.eq(1L) 并不是真正的参数,而是一个 matcher,如下图所示。

    所以如果非要让上面的代码运行相等,只要把 1L 改成 0L 就行了,哈哈哈(PS:这是玩笑话,别学…)。

    Verify

    最后补充下 Verify,这里我就以最简单的 Mocito.times 为例了。UT 如下,验证是否执行了一次。

    when…then… 处的代码就直接忽略了,先从 orderClient.queryById(1L) 方法开始看。

    我们直接跳到 MockHandlerImpl#handle,让我们再回顾下 invocationContainer.setInvocationForPotentialStubbing 这段代码,它会将本次的 invocation 行为存储进 invocationContainer 的 registeredInvocations 中。然后通过 invocationContainer.findAnswerFor 尝试获取一个匹配的 Stub,然后返回结果。

    只需要重点注意,本次的 invocation 行为已经被保存起来了(是不是有点流量录制的味道了?)。

    接下来让我们 Mockito#verify 方法,除掉校验和非主干逻辑,真正核心的就是 mockingProgress.verificationStarted 一行代码:将当前 mock 对象和要 verify 的模式(actualMode)封装成 MockAwareVerificationMode 对象,然后存入 ThreadLocal 中。

    设置完毕后,继续执行后续代码: queryById(1L) ,继续把视野移到 MockHandlerImpl#handle,之前一直被我们无视的代码块终于走到了。因为上一步将 MockAwareVerificationMode 存入到 ThreadLocal 中,因此当执行 mockingProgress().pullVerificationMode() 时就能取出来了。

    首先判断 MockAwareVerificationMode 需要验证的对象和当前对象是否相同(注意这里用等号判断,比较的是地址)。然后构造 VerificationDataImpl,比较简单,如下所示:

    主要的验证就是 verificationMode.verify(data) 这行代码,如果通过后,返回 null 即可;如果未通过,则会抛出异常。

    下面我就以 Times 为例,看看它的 verify 方法实现:

    data.getAllInvocations() :获取直接所有的 Invocation 记录

    data.getTarget() :获取想要验证的 Invocation

    如果 wantedCount > 0,表明需要验证次数,调用 checkMissingInvocation() 方法,防止 getAllInvocations() 中一个匹配的都没有,存在 null 的情况

  • findInvocations():筛选匹配的 Invocation
  • 如果 actualInvocations.isEmpty(),说明一个匹配的都没有,后面代码都是拼装异常了,没啥意思直接跳过
  • 调用 checkNumberOfInvocations() 方法,比对次数,这个太简单就不介绍了。

    Conclusion

    registeredInvocations

    总结下 org.mockito.internal.stubbing.InvocationContainerImpl#registeredInvocations:

    (1)什么时候会有值?

    基本每次调用 proxy 代理类的方法时,都会将当前的 Invocation 存入。

    (2)什么时候消费它?

    每调用一次 when(ConsecutiveStubbing 除外),registeredInvocations 就会 removeLast。

    调用 verify 时,会拿所有的 registeredInvocations,进行比较,但不会移除。

    MockHandlerImpl#handle

    不得不说这个方法太重要了,再总结下它的代码模块吧。

     
    推荐文章
    开朗的打火机  ·  android studio命令行开启项目_android命令行在哪里-CSDN博客
    9 月前
    八块腹肌的小蝌蚪  ·  深瞳丨高校排行“榜”架了谁?-中国科技网
    9 月前
    闯红灯的大熊猫  ·  Python解析xml文件: ElementTree解析xml节点属性排序问题_xml etree 乱序-CSDN博客
    1 年前
    朝气蓬勃的洋葱  ·  virtualization - Finding out the location order of tap interfaces in KVM VM (Libvirt) - Server Fault
    1 年前
    发财的脆皮肠  ·  Springdoc Openapi -添加响应示例值-腾讯云开发者社区-腾讯云
    1 年前
    Link管理   ·   Sov5搜索   ·   小百科
    link管理 - 链接快照平台