相信用 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 或执行单元测试也有可能会失败。
由于时间久远,GitHub Actions 的日志已经被清理了。
一个项目的单元测试如果不能保证稳定通过,那肯定是 测试代码有问题 或者 生产代码存在隐患。
问题复现
来看 ShardingSphere infra-common 模块下的一个单元测试,ShardingSphereMetaDataTest 中有一个用例如下:
@Test
public void assertGetMySQLDefaultSchema() throws SQLException {
MySQLDatabaseType databaseType = new MySQLDatabaseType();
ShardingSphereDatabase actual = ShardingSphereDatabase.create("foo_db", databaseType, Collections.singletonMap("", databaseType), mock(DataSourceProvidedDatabaseConfiguration.class), new ConfigurationProperties(new Properties()), mock(InstanceContext.class));
assertNotNull(actual.getSchema("foo_db"));
}
单独运行这个测试用例,是通过的。
但是,如果运行 infra-common 模块下的所有测试,这个用例就会失败。
其中,ShardingSphereDatabase.create
最终调用的静态方法大致如下,代码中只有正常返回一个 ShardingSphereDatabase
实例或抛出异常两种可能,不存在返回 null
的情况。
private static ShardingSphereDatabase create(final String name, final DatabaseType protocolType, final DatabaseConfiguration databaseConfig, final Collection<ShardingSphereRule> rules, final Map<String, ShardingSphereSchema> schemas) {
// 省略中间过程代码
return new ShardingSphereDatabase(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
泄漏的结论的,如何能够排查出这类泄漏问题?
找时间继续深入探究这个问题。