0


记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程

相信用 Java 写过单元测试的读者们对 Mockito 不会陌生。至于 Mockito 是什么,为什么要用 Mockito,本文不再赘述。本文记录了一次在 Apache ShardingSphere 项目中,由

Mockito.mockStatic

使用不当导致的单元测试偶发报错排查过程。

文章目录

前言

Mockito 自 3.4.0 起新增了一个方法

Mockito.mockStatic

,支持对静态方法 mock。

本人也曾在 Stack Overflow 上回答过一个问题,展示了我在 Apache ShardingSphere 的单元测试代码中使用

Mockito.mockStatic

mock 单例的案例,对

Mockito.mockStatic

方法不是特别熟悉的同学可以了解一下:
如何使用 Mockito mock 单例 Mocking a singleton with mockito

mockStatic

使用有哪些注意实现?我们查看一下 Mockito 官方文档的说明:48. Mocking static methods (since 3.4.0)

When using the inline mock maker, it is possible to mock static method invocations within the current thread and a user-defined scope. This way, Mockito assures that concurrently and sequentially running tests do not interfere. To make sure a static mock remains temporary, it is recommended to define the scope within a try-with-resources construct.

大致的意思是:

mockStatic

方法作用范围是当前线程和用户定义的作用域。为确保

mockStatic

只是临时生效,建议使用 try-with-resources 代码块包裹

mockStatic


解读 Mockito 文档提供的示例:

assertEquals("foo",Foo.method());// 静态方法 Foo.method() 原本行为try(MockedStatic mocked =mockStatic(Foo.class)){// 对 Foo 类进行 mockStatic 
    mocked.when(Foo::method).thenReturn("bar");// 通过 mock 改变静态方法 Foo.method() 行为assertEquals("bar",Foo.method());// 进行测试断言
    mocked.verify(Foo::method);}assertEquals("foo",Foo.method());// 离开 mockStatic 作用域,Foo.method() 恢复原本行为

现在我们思考下,如果

mockStatic

方法没有被包裹在 try-with-resources 代码块中,也没有手动关闭

MockedStatic

对象,会发生什么事情?

根据文档的描述,如果没有关闭

mockStatic

的话,是不是被 mock 的静态类在这条线程上的行为会一直被改变?

Apache ShardingSphere 的单元测试曾出现过因

Mockito.mockStatic

使用后没有释放,导致单元测试偶发失败的问题。

排查过程

Apache ShardingSphere 会通过 GitHub Actions 对每个 PR 或合并到 master 的 commit 运行 CI——标准的 Maven clean install 流程,install 过程中就包括运行单元测试。

有段时间,ShardingSphere 的 CI 偶尔会失败一下,问了一下其他也在参与 ShardingSphere 开发的同学,本地 install 或执行单元测试也有可能会失败。

在这里插入图片描述
https://github.com/apache/shardingsphere/actions/workflows/ci.yml?query=branch%3Amaster+created%3A<2022-07-13+is%3Afailure

由于时间久远,GitHub Actions 的日志已经被清理了。

一个项目的单元测试如果不能保证稳定通过,那肯定是 测试代码有问题 或者 生产代码存在隐患

问题复现

来看 ShardingSphere infra-common 模块下的一个单元测试,ShardingSphereMetaDataTest 中有一个用例如下:

@TestpublicvoidassertGetMySQLDefaultSchema()throwsSQLException{MySQLDatabaseType databaseType =newMySQLDatabaseType();ShardingSphereDatabase actual =ShardingSphereDatabase.create("foo_db", databaseType,Collections.singletonMap("", databaseType),mock(DataSourceProvidedDatabaseConfiguration.class),newConfigurationProperties(newProperties()),mock(InstanceContext.class));assertNotNull(actual.getSchema("foo_db"));}

单独运行这个测试用例,是通过的。
在这里插入图片描述
但是,如果运行 infra-common 模块下的所有测试,这个用例就会失败。
在这里插入图片描述

其中,

ShardingSphereDatabase.create

最终调用的静态方法大致如下,代码中只有正常返回一个

ShardingSphereDatabase

实例或抛出异常两种可能,不存在返回

null

的情况。

privatestaticShardingSphereDatabasecreate(finalString name,finalDatabaseType protocolType,finalDatabaseConfiguration databaseConfig,finalCollection<ShardingSphereRule> rules,finalMap<String,ShardingSphereSchema> schemas){// 省略中间过程代码returnnewShardingSphereDatabase(name, protocolType, resourceMetaData, ruleMetaData, schemas);}

但是,这么简单的一段单元测试确实就报了空指针,而且还是

actual

(静态方法

ShardingSphereDatabase.create

的返回结果)为

null

java.lang.NullPointerException: Cannot invoke "org.apache.shardingsphere.infra.metadata.database.ShardingSphereDatabase.getSchema(String)" because "actual" is null
    at org.apache.shardingsphere.infra.metadata.ShardingSphereMetaDataTest.assertGetMySQLDefaultSchema(ShardingSphereMetaDataTest.java:109)

从代码上看,一个没有可能返回

null

的静态方法,却在单元测试返回了

null

,不理解!

由于本地环境暂时能够持续必现问题,可以打断点 Debug 一下。

失败是偶发而不是必现的原因是:一个模块下的单元测试的运行顺序不是恒定的。 有些可能污染其他测试用例的测试代码,恰好其运行顺序比较靠后,测试运行表现为正常通过。

曾经我也解决过另一个受单元测试执行顺序影响的偶发问题,具体排查可以见我之前的文章:记一次 ThreadLocal 泄漏导致的 shardingsphere-jdbc-core 单元测试偶发失败的排查与修复

调试代码

打上断点,运行模块全量测试,跑到了断言失败前的代码。
来一个快速表达式计算,确实是

ShardingSphereDatabase.create

方法返回了

null


在这里插入图片描述

那进入方法内部看看:

发现端倪 & 解决

奇怪的现象出现了!可以看下面这个动图:
在这里插入图片描述
进入

ShardingSphereDatabaes.create

方法后,点击

Step Into

,正常情况下应该继续进入

create

方法第一行代码的

DatabaseRulesBuilder.build

方法,但是,调试器却直接跳到了

create

方法的

return

,并且点击

Step Into

也没有继续进入

create

方法!

这种奇怪的现象,凭经验来看,有可能是实际运行的字节码与源码对不上。代码中全局搜了一下

mockStatic

方法的使用,果然发现了一些单元测试代码使用了

mockStatic

方法,但既没有使用 try-with-resources,又没有手动释放。

于是,我对

mockStatic

使用不当的代码进行了修复,并且在 ShardingSphere 的代码规范里面补充了使用

mockStatic

mockConstruction

的要求。

具体可见:

  • 修复 ShardingSphere 单元测试 mockStatic 泄漏:Fix mockStatic leak in unit tests #19077
  • 更新 ShardingSphere 关于 Mockito 使用的代码规范:Update Code of Conduct about Mockito #19083

挖坑

在前面的步骤已经发现并解决了单元测试的问题,但这是凭个人经验和运气解决的。

假如我是曾经没有使用过

mockStatic

等方法、没有相关经验的开发者,光凭 IDEA 的 Debug 现象是无法直接得出

mockStatic

泄漏的结论的,如何能够排查出这类泄漏问题?

找时间继续深入探究这个问题。


本文转载自: https://blog.csdn.net/wu_weijie/article/details/125759460
版权归原作者 [email protected] 所有, 如有侵权,请联系我们删除。

“记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程”的评论:

还没有评论