阿里妹导读
本文探讨了从使用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
如何去除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 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 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 ExampleTestClass { ... @Test public void test() { ... } ...}使用Mockito Only的情况下,我们可以结合Junit的ClassRule实现同样的效果,下面代码是上面例子对应的Mockito Only的实现。
public 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 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)来生成模拟对象。其实现原理主要包含以下几个方面:
动态代理:对于接口类型的模拟对象,Mockito使用Java的动态代理机制(java.lang.reflect.Proxy)来生成代理类。这种方式依赖于Java反射API,能在运行时生成代理实例,从而替代真实的实现。字节码操作:对于具体类(非接口)的模拟对象,Mockito使用ByteBuddy库来操作字节码。ByteBuddy允许在运行时生成新的类或者修改现有的类,用来代理原类的行为。方法拦截:无论是通过动态代理还是字节码操作生成的代理对象,Mockito都会使用方法拦截器(interceptor)来捕获方法调用。拦截器会检查配置的行为(如返回值、异常抛出等),并根据这些配置响应方法调用。行为记录:Mockito还会记录每个模拟对象的方法调用,这使得它可以在测试中验证某些方法是否按照预期被调用。PowerMock
PowerMock是一个更强大的测试框架,通常与Mockito、EasyMock等其他模拟框架一起使用。它能够模拟一些Mockito不能处理的情况,比如静态方法、构造函数和私有方法。PowerMock的实现原理更为复杂,主要通过以下方面实现:
字节码操纵(Bytecode Manipulation):与Mockito类似,PowerMock也使用字节码操作技术,但它主要依赖于Java Assist和CGLib库来修改字节码。通过这些库,PowerMock可以生成代理类,并能对类的字节码进行修改,包括静态方法和构造函数。类加载器(ClassLoader)替换:PowerMock会使用自定义的类加载器来加载被测试的类。这个自定义类加载器能够修改类的字节码,从而允许对类的修改进行跟踪和控制。这是PowerMock能够绕过JVM对某些类和方法进行的限制,无缝地插入自己的逻辑的重要原因。JVM层面的代理和拦截:为了模拟静态方法和私有方法,PowerMock在JVM层面进行代理和拦截。这意味着它可以通过修改类的字节码来重定向静态方法调用,甚至可以替换构造函数,以便在创建对象时插入模拟行为。集成其他模拟框架:PowerMock通常与Mockito或EasyMock集成使用。它主要负责那些普通的模拟框架无法处理的特殊情况,如静态方法、私有方法的模拟。通过扩展这些框架的功能,PowerMock大大增强了测试的灵活性和能力。总结
Mockito:主要通过动态代理和字节码生成来生成模拟对象,只能处理接口或具体类的非静态、非私有方法,较为轻量。PowerMock:通过字节码操作、类加载器替换等高级技术来处理静态方法、构造函数和私有方法等复杂情况,能够扩展Mockito、EasyMock等框架的功能,功能更强大但实现更为复杂。这两者的底层实现技术选择使得它们在功能和应用场景上有所区别。尽管PowerMock能做更多“黑魔法”般的操作,但Mockito的简单和性能优势使其在大多数日常测试中更受欢迎。