Mockito 实现原理(3):如何对 final 类进行 mock

您所在的位置:网站首页 system类的mock Mockito 实现原理(3):如何对 final 类进行 mock

Mockito 实现原理(3):如何对 final 类进行 mock

2024-07-11 15:49| 来源: 网络整理| 查看: 265

目录 背景方法正常方法阅读源码时可以使用的方法 什么情况下可以不创建派生类原理小结

背景

前面两篇提到,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 行 @Override public byte[] transform( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (classBeingRedefined == null || !mocked.contains(classBeingRedefined) && !flatMocked.contains(classBeingRedefined) || EXCLUDES.contains(classBeingRedefined)) { return null; } 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(new ParameterWritingVisitorWrapper(classBeingRedefined)) .visit(mockTransformer) .make() .getBytes(); } catch (Throwable throwable) { lastException = throwable; return null; } } }

这个 transform 实现的是 java.lang.instrument.ClassFileTransformer 接口中的同名方法,总之就是对现有类的字节码进行修改,然后重新加载这个类。transform 就是用来“对现有类的字节码进行修改”的钩子方法。

不出意外,transform 也是借助了 Byte Buddy 实现了字节码修改,所以源码中没有体现出来很复杂的东西,真正复杂的地方都在 Byte Buddy 里了。

这里我还没完全看懂,反正无论如何,和前两篇讲到的 SubclassByteBuddyMockMaker 差不多,Mockito 会“植入”一个拦截器,这样你在调用 mock 对象的任何方法时都会走到这个拦截器里。这个拦截器会记录每次调用的信息,可以设置预期返回结果,等等。

小结

一个问题是,既然 InlineByteBuddyMockMaker 本身以 SubclassByteBuddyMockMaker 作为兜底,又增加了对 final 类/方法的支持,为什么不用它作为默认的 MockMaker 实现呢?还非得通过复杂的配置来切换。

原因大概是,InlineByteBuddyMockMaker 会对类本身进行修改,这不是一件好事,如果处理不当可能会带来意外问题。所以默认情况下,Mockito 是不鼓励用 InlineByteBuddyMockMaker 的,考虑到对 final 类/方法进行 mock 的需求不大,需要通过配置来实现也在情理之中了。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3