目录
背景
最近开始读 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()
调用会走到拦截器中 handler
的 handle
方法中,然后会记录下这次的调用,包括调用的方法是什么(这个例子中是 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 覆盖一下不就行了,哪儿用得着这么大费周章。