目录
背景
前面两篇提到,Mockito 默认基于创建派生类(subclass)来实现 mock(包括 spy)。
那么问题来了,如果我的类标记为 final,明确禁止创建派生类,那不就没法 mock 了吗?
为了解决这个问题,Mockito 2 中引入了
InlineByteBuddyMockMaker
。和前面讨论过的默认的
SubclassByteBuddyMockMaker
相比,这个
InlineByteBuddyMockMaker
同样基于 Byte Buddy 这个提供 Java 字节码操作功能的第三方库,但会尽量不通过创建派生类来实现 mock。
(注:本文基于 Mockito 4.6.1 源码)
方法
正常方法
对 final 类进行 mock,需要用
InlineByteBuddyMockMaker
替换掉默认的
SubclassByteBuddyMockMaker
。
替换方法是通过创建一个配置文件。按照这篇教程,应该是在
src/test/resources/mockito-extensions
这个目录下,创建一个名为
org.mockito.plugins.MockMaker
的文件(这个名字其实就是 MockMaker 接口,我们其实就是在为这个接口指定一个实现,否则就会用默认的
SubclassByteBuddyMockMaker
实现了),然后在这个文件里写入:
mock-maker-inline
或者(下面这个是我在源码注释中看到的,其实就是我们要使用的实现类):
org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
阅读源码时可以使用的方法
不幸的是,我用这个方法暂时还没有成功。但因为我是在研究 Mockito 的源码,所以我直接修改了源码中的这个文件:
// org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java// 第 29 行开始
DEFAULT_PLUGINS.put(MockMaker.class.getName(),"org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker");// 改之前
把最下面一行改成了:
// org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java// 第 29 行开始
DEFAULT_PLUGINS.put(MockMaker.class.getName(),"org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker");// 改之后
这样也实现了替换效果。
什么情况下可以不创建派生类
这个
InlineByteBuddyMockMaker
也不是万能的。在某些情况下,不创建派生类是行不通的,于是它本身会使用
SubclassByteBuddyMockMaker
作为兜底。
这些必须要创建派生类的情况包括:
- 被 mock 的类是抽象类;
- mock 时,设置了要额外实现的接口(这是 Mockito 的一个功能);
- mock 时,显性设置其支持序列化;
正常情况下我们就是直接 mock,不会设置什么要额外实现的接口或者序列化之类的。所以不用担心,我们的 final 类(或方法)通常来说就是可以 mock 的!
原理
InlineByteBuddyMockMaker
基于 Java Instrumentation API ,这是 Java 提供的一个可以像现有的已编译的类添加字节码的功能。
具体在源码中的体现:
// org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java// 第 371 行@Overridepublicbyte[]transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer){if(classBeingRedefined ==null||!mocked.contains(classBeingRedefined)&&!flatMocked.contains(classBeingRedefined)|| EXCLUDES.contains(classBeingRedefined)){returnnull;}else{try{return byteBuddy
.redefine(
classBeingRedefined,// new ClassFileLocator.Compound(ClassFileLocator.Simple.of(
classBeingRedefined.getName(), classfileBuffer)// ,ClassFileLocator.ForClassLoader.ofSystemLoader()// ))// Note: The VM erases parameter meta data from the provided class file// (bug). We just add this information manually..visit(newParameterWritingVisitorWrapper(classBeingRedefined)).visit(mockTransformer).make().getBytes();}catch(Throwable throwable){
lastException = throwable;returnnull;}}}
这个
transform
实现的是
java.lang.instrument.ClassFileTransformer
接口中的同名方法,总之就是对现有类的字节码进行修改,然后重新加载这个类。
transform
就是用来“对现有类的字节码进行修改”的钩子方法。
不出意外,
transform
也是借助了 Byte Buddy 实现了字节码修改,所以源码中没有体现出来很复杂的东西,真正复杂的地方都在 Byte Buddy 里了。
这里我还没完全看懂,反正无论如何,和前两篇讲到的
SubclassByteBuddyMockMaker
差不多,Mockito 会“植入”一个拦截器,这样你在调用 mock 对象的任何方法时都会走到这个拦截器里。这个拦截器会记录每次调用的信息,可以设置预期返回结果,等等。
小结
一个问题是,既然
InlineByteBuddyMockMaker
本身以
SubclassByteBuddyMockMaker
作为兜底,又增加了对 final 类/方法的支持,为什么不用它作为默认的 MockMaker 实现呢?还非得通过复杂的配置来切换。
原因大概是,
InlineByteBuddyMockMaker
会对类本身进行修改,这不是一件好事,如果处理不当可能会带来意外问题。所以默认情况下,Mockito 是不鼓励用
InlineByteBuddyMockMaker
的,考虑到对 final 类/方法进行 mock 的需求不大,需要通过配置来实现也在情理之中了。
版权归原作者 倪琛 所有, 如有侵权,请联系我们删除。