Mockito 实现原理(1):基本原理

背景

最近开始读 Mockito 的源码,还挺有意思。先简单说说它的基本原理吧!

(注:本文基于 Mockito 4.6.1 源码

基本功能是如何实现的

Mockito 最基本的用法就是 mock 一个对象,然后在调用其方法时返回指定结果。

以一段最简单的代码为例:

List mockList = Mockito.mock(List.class); // 使用 Mockito 创建一个 mock List
assert 0 == mockList.size(); // mockList.size() 默认返回 0
Mockito.when(mockList.size()).thenReturn(100); // 指定 mockList.size() 返回 100
assert 100 == mockList.size(); // 再次调用 mockList.size(),果然返回 100 了

这个效果是如何实现的呢?

默认实现方法:SubclassByteBuddyMockMaker

事实上,Mockito 提供了不止一种方法来实现上面的 mock 效果。在 org/mockito/plugins/MockMaker.java 中定义了一个 MockMaker 接口:

public interface MockMaker {
	// 略
}

这个接口有好多个实现类。目前我执行时发现,默认用的是 SubclassByteBuddyMockMaker 这个实现,所以下面就讨论该实现的具体步骤。

步骤 1:创建 mock 类

第一行调用 Mockito.mock(List.class) 时,SubclassByteBuddyMockMaker 首先会做的就是创建一个 mock 类。

毕竟,List.class 是一个接口,无法实例化,所以需要一个 mock 类来实现 List 接口,然后实例化该类,这才得到 mockList

事实上,即便你的调用方式是 Mockito.mock(ArrayList.class),即传递一个具体类而不是接口,Mockito 也仍然会创建一个新的 mock 类,让该 mock 类继承 ArrayList,然后再实例化。

为什么要这么做呢?因为 SubclassByteBuddyMockMaker 会给这个新的 mock 类加一些功能,实现某些额外接口。直接改动用户传来的类显然不行,所以就需要新创建一个子类,继承用户传来的类,然后再添加功能。

这也是 SubclassByteBuddyMockMaker 前缀的由来:创建一个子类(subclass)来实现 mock。

在源码中,这个 mock 类叫做 mockedProxyType

// org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java

// 第 41 行
@Override
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
	// 这一行创建了 mockedProxyType,即用来实现 mock 的子类或实现类
	// settings 包含了全部所需信息,包括被 mock 对象的类(在本例中就是 List)
    Class<? extends T> mockedProxyType = createMockType(settings);
    // 以下略
}

那么,这个类是如何被创建的呢?说实话,这是个比较麻烦的过程,因为这个类在编译时是不存在的,是运行时动态创建出来的。

SubclassByteBuddyMockMaker 的实现基于 Byte Buddy 这个第三方库,这个库的功能就是动态创建 Java 类。其原理是基于字节码层面的 hack,比较复杂,就不必深究了。

总之,通过对 Byte Buddy API 的一通操作, SubclassByteBuddyMockMaker 就成功实现了这个 mockedProxyType 类的动态创建,并且还使用了 Byte Buddy 提供的缓存功能。即,这个类一旦被创建,如果之后再有同样的 mock 类型需要使用,就不会重复创建了。

步骤 2:创建 mock 实例

好了,现在有了 mockedProxyType,可以实例化了!

和创建 mock 类时一样,对于实例化,Mockito 也有多钟不同实现方式。我在 debugger 里看到的默认实现叫做 ObjenesisInstantiator,是基于 Objenesis (名字起得很好,Obj + Genesis,Genesis 是创世记)这个第三方库的,这个库专门用来做实例化,可以在避免执行构建函数的情况下实现类的实例化。

避免执行构建函数,这个目的很好理解,因为用户提供的 mock 类的构建函数里可能有不方便执行的操作,比如连接数据库(可能这就是用户选择 mock 掉它的原因);但我不太理解的是,既然 mockedProxyType 是子类,那么在构建函数里选择不执行父类(即用户提供的类)的构建函数不就可以了吗?为什么非要用 Objenesis 呢?

这个问题暂时还没找到答案,不过似乎也不太重要。总之,实例化也顺利完成了,下面是重点了。

步骤 3:记录预期返回

回到我们一开始的例子:

List mockList = Mockito.mock(List.class); // 使用 Mockito 创建一个 mock List
assert 0 == mockList.size(); // mockList.size() 默认返回 0
Mockito.when(mockList.size()).thenReturn(100); // 指定 mockList.size() 返回 100
assert 100 == mockList.size(); // 再次调用 mockList.size(),果然返回 100 了

下面来看看第三行是怎么做到让 mockList 知道自己要返回 100 的。

前面提到过,SubclassByteBuddyMockMaker 之所以要给被 mock 类创建一个子类(或实现类),是因为要加一些功能。这个“加上去”的功能,就是一个拦截器,这个拦截器作为 mockitoInterceptor 字段被加到了 mock 类中。

事实上,你的 mockList.size() 调用会走到拦截器中 handlerhandle 方法中,然后会记录下这次的调用,包括调用的方法是什么(这个例子中是 size)、参数是什么(这个例子中是无)。

然后在你调用 Mockito.when 时,Mockito 会找到最近的这次调用;在你调用 .thenReturn(100) 时,Mockito 会记下来,下次在遇到相同调用时,就返回 100。就是这么简单。

事实上,正是由于 mock 对象的方法调用和 when 之间是通过“记录”而不是返回值来沟通的,事实上你也可以这么写:

List mockList = Mockito.mock(List.class);
mockList.size(); // 记录下这次调用
Mockito.when(0).thenReturn(100); // 指定再遇到上次的调用时,返回 100
assert 100 == mockList.size(); // 再次调用 mockList.size(),果然返回 100 了

步骤 4:返回预设结果

这一步就比较简单了,当你再次调用 mockList.size() 时,依然会走到 Mockito 的拦截器里,然后一看,之前已经预设过这个调用的预期返回结果是 100 了!那就直接返回 100 吧。

小结

看完这部分源码之后发现,其实 mock 的基本逻辑还是比较简单的,最大的麻烦就是如何“动态创建你需要的类”。

可以想见,在 JavaScript 之类的动态语言中,没有编译器对类型的限制,同样的功能该会多容易实现!直接把对象相应的方法 property 覆盖一下不就行了,哪儿用得着这么大费周章。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值