JDK11下Mock框架进化:从PowerMockito到Mockito Only

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

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

为什么要去除PowerMock依赖?

这个契机来自于升级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

image.png

如何去除PowerMock依赖?

要想去除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() {
        ...
    }
    ...
}

Mock静态方法

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));


Mockito版本升级不兼容变更

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 from anything i.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 for anything and for cast avoidance)
  • anyX like anyString will check the arg is not null and that has the correct type
  • anyList will check the argument is not null and a List instance
  • anyListOf (and the likes) at the moment are just aliases to their non generic counter part like anyList

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 the any familly should allow null and if not do a type check. For example with these matchers :


  • any, anyObject stay the same, they currently allow null and don’t have to do type check anyway
  • any(Class) currently allows null and doesn’t do type check => allows null and if not checks for the given type
  • any<Collection>Of currently doesn’t allow null and does a type check of the collection, not elements => allows null, if not checks collection type, if not empty checks element type

Maybe extend the isA family that won’t allow any null arguments. 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在多线程测试场景中的限制,这些不兼容性问题还是需要在验证单元测试的时候一个个解决。

实现Mockito Only之后的效果

测试类更加专注

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

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

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

PowerMock和Mockito的实现原理区别

内容来自GTP-4o

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


Mockito

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

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


PowerMock

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大大增强了测试的灵活性和能力。


总结

  • Mockito:主要通过动态代理和字节码生成来生成模拟对象,只能处理接口或具体类的非静态、非私有方法,较为轻量。
  • PowerMock:通过字节码操作、类加载器替换等高级技术来处理静态方法、构造函数和私有方法等复杂情况,能够扩展Mockito、EasyMock等框架的功能,功能更强大但实现更为复杂。

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







来源  |  阿里云开发者公众号
作者  |  左冰

相关文章
源码分析系列教程(12) - 手写Map框架(基于JDK1.7)
源码分析系列教程(12) - 手写Map框架(基于JDK1.7)
37 0
|
3月前
|
Java
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
|
5月前
|
前端开发 Java 应用服务中间件
Spring框架第六章(SpringMVC概括及基于JDK21与Tomcat10创建SpringMVC程序)
Spring框架第六章(SpringMVC概括及基于JDK21与Tomcat10创建SpringMVC程序)
|
5月前
|
XML Java 数据格式
【JAVA日志框架】JUL,JDK原生日志框架详解。
【JAVA日志框架】JUL,JDK原生日志框架详解。
39 0
|
缓存 自然语言处理 Rust
比JDK最高快170倍,蚂蚁集团开源高性能多语言序列化框架Fury
Fury是一个基于JIT动态编译和零拷贝的多语言序列化框架,支持Java/Python/Golang/JavaScript/C++等语言,提供全自动的对象多语言/跨语言序列化能力,和相比JDK最高170倍的性能。经过多年蚂蚁核心场景的锤炼打磨,现已正式在Github对外开源:https://github.com/alipay/fury
2579 5
|
Oracle Java 关系型数据库
eclipse 环境 JDK1.8 换成 JDk1.7【以及此错误only available on Java 1.5 and highe 】
eclipse 环境 JDK1.8 换成 JDk1.7【以及此错误only available on Java 1.5 and highe 】
160 0
eclipse 环境 JDK1.8 换成 JDk1.7【以及此错误only available on Java 1.5 and highe 】
|
Oracle Java 关系型数据库
JDK/JAVA Exception NSWindow drag regions should only be invalidated on the Main Thread
JDK/JAVA Exception NSWindow drag regions should only be invalidated on the Main Thread
151 0
|
Java API Maven
01、JUL日志(JDK自带日志框架,包含源码分析)(二)
01、JUL日志(JDK自带日志框架,包含源码分析)(二)
01、JUL日志(JDK自带日志框架,包含源码分析)(二)
|
Java 数据库
01、JUL日志(JDK自带日志框架,包含源码分析)(一)
01、JUL日志(JDK自带日志框架,包含源码分析)(一)
01、JUL日志(JDK自带日志框架,包含源码分析)(一)
|
机器学习/深度学习 并行计算 算法
【jdk8新特性】Fork_Join框架介绍
【jdk8新特性】Fork_Join框架介绍
106 0