Posted in

JDK11 下 Mock 框架进化:从 PowerMockito 到 Mockito Only_AI阅读总结 — 包阅AI

包阅导读总结

1.

关键词:JDK11、Mockito、PowerMock、单元测试、框架进化

2.

总结:本文探讨从PowerMock迁移到Mockito Only的必要性和实践方法,指出因JDK11升级和PowerMock的不维护,介绍了替代过程中的改动、限制及提效工具,还对比了两者底层实现原理。

3.

主要内容:

– 从PowerMock迁移到Mockito Only的原因

– PowerMock只支持JDK9且长期不维护

– 避免JDK新版本新特性无法使用

– 迁移的改动

– 去除PowerMock依赖,用Mockito替代

– 处理Mockito版本升级带来的变化

– Mockito Only的使用

– JUnit Runner的设置

– 不支持Mock private和final方法及变量的处理

– Mock规则多处复用的实现

– 多线程Mock的限制及部分解决方案

– 版本升级的不兼容变更及提效工具

– anyXXX()匹配行为等变化

– 单元测试重构的AI和脚本方案

– Mockito和PowerMock底层实现原理的区别

思维导图:

文章地址:https://mp.weixin.qq.com/s/OsySrzocrMmJdk6C0_h60A

文章来源:mp.weixin.qq.com

作者:左冰

发布时间:2024/8/19 10:08

语言:中文

总字数:4646字

预计阅读时间:19分钟

评分:84分

标签:测试框架,Mockito,JDK11,单元测试,代码重构


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

本文探讨了从使用PowerMock的测试环境迁移到仅使用Mockito(Mockito Only)策略的必要性和实践方法。

TL;DR: 为了写出更好的代码和延长生命,请尽快脱离PowerMock的泥潭,拥抱Mockito Only的海洋。

这个契机来自于升级JDK11,在给团队中的一个核心应用升级JDK11时调研发现,PowerMock从文档来看只支持JDK9,不支持更高JDK版本,更重要的是PowerMockito已经长期不维护了。

如果继续在项目里面集成一个已经不维护的开源测试框架,后续极有可能出现JDK新版本新特性无法使用的问题,因此趁着JDK11升级这样一次大刀阔斧改造的机会,去除PowerMock依赖,使用Mockito Only测试框架。

根据Mockito开源仓库的文档介绍,Mockito一直在迭代新的版本,测试框架也在不断适配新的JDK版本。

https://github.com/powermock/powermock

https://github.com/mockito/mockito/releases/tag/v5.0.0

另外可以分享给正在使用PowerMock的团队一点经验,在尝试升级PowerMock版本时,发现PowerMock存在内存泄漏的问题,PowerMock社区有用户反馈有类似的问题,对应的issue一直没有被解决。

https://github.com/powermock/powermock/issues/227

要想去除PowerMock依赖,大的改动其实就两部分,一部分是PowerMock依赖的去除,对应的Mock功能需要使用Mockito来替代,另一部分是Mockito本身版本升级带来的改动。

Mockito Only替代PowerMockito

JUnit Runner

使用Mockito Only的时候,JUnit Runner需要使用:

@RunWith(MockitoJUnitRunner.class)

另外在需要使用spring-test测试框架的场景中Mockito没有——

PowerMock @PowerMockRunnerDelegate类似的注解,不过我们可以在测试类里面配置Mockito Junit Rule实现同样的效果。

public class ExampleTestClass {
@Rule public MockitoRule mockito = MockitoJUnit.rule(); ...
@Test public void test() { ... } ...}

Mockito最新版本也支持Mock静态方法,用法和PowerMock一样。

Mock private和final方法

Mockito不支持Mock private和final方法,这个需要在改造时对代码做一些重构,PowerMock在这种场景下太好用了,助长了冗长且难以测试的代码出现。

Mock private和final变量

Mockito不支持设置private和final变量,PowerMock的Whitebox无法再使用,不过我们可以利用其它三方库曲线救国,比如apache common包里面的FieldUtils,不过只能设置private变量,final变量还是需要重构代码。

Mock规则多处复用

Mock规则复用是指,为了精简单元测试和提升编写单元测试的效率,我们可以抽取出单元测试中重复的Mock规则,实现一次编写,多处复用。

PowerMock是通过PowerMockPolicy类实现,举个例子:

public class ContextMockPolicy implements PowerMockPolicy {  @Override  public void applyClassLoadingPolicy(MockPolicyClassLoadingSettings settings) {    settings.addFullyQualifiedNamesOfClassesToLoadByMockClassloader(        Xxx.class.getName());  }
@Override public void applyInterceptionPolicy(MockPolicyInterceptionSettings settings) { final Method getXxx = Whitebox.getMethod(Xxx.class, "getXxx"); settings.stubMethod(getXxx, Optional.ofNullable(mockXxx());
final Method getXxxXxx = Whitebox.getMethod(Xxx.class, "getXxxXxx"); settings.stubMethod(getXxxXxx, Optional.ofNullable(Xxx)); }}
@MockPolicy({ContextMockPolicy.class})public class ExampleTestClass {  ...
@Test public void test() { ... } ...}

使用Mockito Only的情况下,我们可以结合Junit的ClassRule实现同样的效果,下面代码是上面例子对应的Mockito Only的实现。

public class ContextMockRule implements TestRule {  private MockedStatic<Xxx> mockedStatic;
@Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { mockXxx(); base.evaluate(); } finally { if (mockedStatic != null) { mockedStatic.close(); } } } }; }
private void mockXxx() { mockedStatic = Mockito.mockStatic(Xxx.class);
mockedStatic .when(() -> Xxx.getXxx()) .thenReturn(Optional.ofNullable(mockXxx())); mockedStatic .when(() -> Xxx.getXxxXxx()) .thenReturn(Optional.ofNullable(Xxx)); }}
public class ExampleTestClass {
@ClassRule public static ContextMockRule contextMockRule = new ContextMockRule(); ...
@Test public void test() { ... } ...}

Mockito Only多线程Mock的限制

Mockito在多线程的测试场景下,包括ExecutorService和ParallelStream,存在静态方法Mock不生效的问题,并未对齐PowerMock。

其中ExecutorService线程池并发场景,我们可以采用下面Mock ExecutorService的方式解决,但是对于Java Stream ParallelStream的并发场景,还未找到可行的解决方案。

ExecutorService chatExecutor = Mockito.mock(ExecutorService.class);doAnswer(    (Answer<Object>)        invocation -> {          Object[] args = invocation.getArguments();          Callable callable = (Callable) args[0];          Object result = callable.call();
FutureTask futureTask = Mockito.mock(FutureTask.class); Mockito.when(futureTask.get(anyLong(), any())) .thenReturn(result); return futureTask; }).when(chatExecutor).submit(any(Callable.class));

https://groups.google.com/g/mockito/c/8_WGBB3Jbtk/m/JUUq4EpgplcJ

I’d like to give additional info on this. The origin of these methods is they come fromanythingi.e. anything matches, later for shortness and cast avoidance the aliases grew, but the API naming thus became inconsistent with what a human would expect. So this behavior is being changed in mockito 2 beta, to be precise here’s the status on these API in the version 2.0.5-beta :


  • any,anyObject,any(Class)won’t check anything (at first they were just aliases foranythingand for cast avoidance)

  • anyXlikeanyStringwill check the arg is not null and that has the correct type

  • anyListwill check the argument is not null and aListinstance

  • anyListOf(and the likes) at the moment are just aliases to their non generic counter part likeanyList

Note this is work in progress (started here in#141), these new behavior can / will change in the beta phase. I’m especially wondering if theanyfamilly should allownulland if not do a type check. For example with these matchers :


  • any,anyObjectstay the same, they currently allownulland don’t have to do type check anyway

  • any(Class)currently allowsnulland doesn’t do type check => allowsnulland if not checks for the given type

  • any<Collection>Ofcurrently doesn’t allownulland does a type check of the collection, not elements => allowsnull, if not checks collection type, if not empty checks element type

Maybe extend theisAfamily that won’t allow anynullarguments. Thoughts ?

引用Mockito Google Group里面对Mockito版本升级之后行为变化的讨论,结合自己在做项目改造时的经验,发现了下面这些不兼容的变更。

anyXXX() 匹配行为变化

anyLong(),anyString()和anyObject()等包含类型判断的匹配方法不再支持null值,如果是null值,需要修改成“any()”匹配方法。

参数匹配器输入类型变化

ArgumentMatcher匿名类方法明确了参数具体类型,不再是Object。

获取Invocation参数行为变化

获取Invocation调用参数方式的变化。

话说不会偷懒的工程师不是一位好产品,对于那些已经开发维护数年的应用,可能项目里面有成百上千个单元测试。如果纯手工重构,那耗费的精力可能会把一个好产品熬走。下面介绍一些可以提效的工具,如果有更好的选择,欢迎大佬评论。

AI Agent

如果你的项目里面单元测试比较少,那可以使用AI来减轻你的负担。

在重构测试代码的场景下,我们可以定义自己的Prompt,实现测试类的自动重构。不过由于大语言模型本身的特点,输入输出越长,生成完整答案的时间就越长,这就导致重构测试代码的速度非常缓慢,更别说有时候生成的内容也不尽符合要求。

也许我们可以采用更好的Agent方案,但是这个需要开发和针对实际测试场景进行大量的调试。

单元测试重构脚本

针对上千个单元测试的应用,为了加快重构速度,上面的Prompt方案就不可行了。

分享一下我的解决方案,在重构过程中,先通过手工重构几个单元测试,发现了一些可以自动化的步骤,比如替换注解,最终基于Python和Tree-sitter实现了一个单元测试重构脚本,这样可以减少大部分的重复劳动。

不过这个自动化脚本还是解决不了一些特殊的不兼容问题,比如Mockito在多线程测试场景中的限制,这些不兼容性问题还是需要在验证单元测试的时候一个个解决。

测试类更加专注

少了很多PowerMock专用注解,并且Mock资源的创建和释放都有迹可循,不需要担心内存泄漏问题。

单元测试运行性能大幅提升

单元测试任务耗时显著减少,在我的工作电脑上运行,从改造前的9分53秒减少到3分57秒,占用内存从5G左右下降到2.5G左右。

Mockito和PowerMock都是常用的Java单元测试框架,用于创建和管理模拟对象(mock objects),但它们在底层实现原理上有一些显著的区别。

Mockito是一个相对轻量级的测试框架,主要通过Java的动态代理和字节码生成库(如ByteBuddy)来生成模拟对象。其实现原理主要包含以下几个方面:

  1. 动态代理:对于接口类型的模拟对象,Mockito使用Java的动态代理机制(java.lang.reflect.Proxy)来生成代理类。这种方式依赖于Java反射API,能在运行时生成代理实例,从而替代真实的实现。
  2. 字节码操作:对于具体类(非接口)的模拟对象,Mockito使用ByteBuddy库来操作字节码。ByteBuddy允许在运行时生成新的类或者修改现有的类,用来代理原类的行为。
  3. 方法拦截:无论是通过动态代理还是字节码操作生成的代理对象,Mockito都会使用方法拦截器(interceptor)来捕获方法调用。拦截器会检查配置的行为(如返回值、异常抛出等),并根据这些配置响应方法调用。
  4. 行为记录:Mockito还会记录每个模拟对象的方法调用,这使得它可以在测试中验证某些方法是否按照预期被调用。

PowerMock是一个更强大的测试框架,通常与Mockito、EasyMock等其他模拟框架一起使用。它能够模拟一些Mockito不能处理的情况,比如静态方法、构造函数和私有方法。PowerMock的实现原理更为复杂,主要通过以下方面实现:

  1. 字节码操纵(Bytecode Manipulation):与Mockito类似,PowerMock也使用字节码操作技术,但它主要依赖于Java Assist和CGLib库来修改字节码。通过这些库,PowerMock可以生成代理类,并能对类的字节码进行修改,包括静态方法和构造函数。
  2. 类加载器(ClassLoader)替换:PowerMock会使用自定义的类加载器来加载被测试的类。这个自定义类加载器能够修改类的字节码,从而允许对类的修改进行跟踪和控制。这是PowerMock能够绕过JVM对某些类和方法进行的限制,无缝地插入自己的逻辑的重要原因。
  3. JVM层面的代理和拦截:为了模拟静态方法和私有方法,PowerMock在JVM层面进行代理和拦截。这意味着它可以通过修改类的字节码来重定向静态方法调用,甚至可以替换构造函数,以便在创建对象时插入模拟行为。
  4. 集成其他模拟框架:PowerMock通常与Mockito或EasyMock集成使用。它主要负责那些普通的模拟框架无法处理的特殊情况,如静态方法、私有方法的模拟。通过扩展这些框架的功能,PowerMock大大增强了测试的灵活性和能力。

这两者的底层实现技术选择使得它们在功能和应用场景上有所区别。尽管PowerMock能做更多“黑魔法”般的操作,但Mockito的简单和性能优势使其在大多数日常测试中更受欢迎。

本方案结合通义千问和LangChain技术构建高效的对话模型,该模型基于自然语言处理技术提升语义理解和用户交互体验。它可以有效解决传统对话模型在理解能力和交互效果上的局限,使得用户沟通更加自然流畅,被广泛应用于聊天机器人、智能客服和社交媒体等多种场景。

点击阅读原文查看详情。