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

这篇具有很好参考价值的文章主要介绍了记一次 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 或执行单元测试也有可能会失败。

记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程
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 中有一个用例如下:

@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"));
}

单独运行这个测试用例,是通过的。
记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程
但是,如果运行 infra-common 模块下的所有测试,这个用例就会失败。
记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程

其中,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
记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程

那进入方法内部看看:

发现端倪 & 解决

奇怪的现象出现了!可以看下面这个动图:
记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程
进入 ShardingSphereDatabaes.create 方法后,点击 Step Into,正常情况下应该继续进入 create 方法第一行代码的 DatabaseRulesBuilder.build 方法,但是,调试器却直接跳到了 create 方法的 return,并且点击 Step Into 也没有继续进入 create 方法!

这种奇怪的现象,凭经验来看,有可能是实际运行的字节码与源码对不上。代码中全局搜了一下 mockStatic 方法的使用,果然发现了一些单元测试代码使用了 mockStatic 方法,但既没有使用 try-with-resources,又没有手动释放。

于是,我对 mockStatic 使用不当的代码进行了修复,并且在 ShardingSphere 的代码规范里面补充了使用 mockStaticmockConstruction 的要求。

具体可见:

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

挖坑

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

假如我是曾经没有使用过 mockStatic 等方法、没有相关经验的开发者,光凭 IDEA 的 Debug 现象是无法直接得出 mockStatic 泄漏的结论的,如何能够排查出这类泄漏问题?

找时间继续深入探究这个问题。文章来源地址https://www.toymoban.com/news/detail-495381.html

到了这里,关于记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Mockito mockStatic, mockConstruction

    之前项目中都是使用PowerMock的mockStatic和whenNew来mock 静态方法和构造函数。但是在升级jdk17的过程中发现PowerMock已停止维护且不支持jdk17,我们转而将Mockito升级,使用Mockito中提供的 Mockito.mockStatic , Mockito.mockConstruction 。 首先来看一下较为简单的mockStatic方法 第一步先声明一个

    2024年02月11日
    浏览(35)
  • 记一次javaMetaspace导致CPU200%的排查

    insertMotionDataByWxCallBack方法并发多(其实也没多少,可能就3个?)就导致CPU200%了,本地没法复现。 看报错是:java.lang.OutOfMemoryError: Metaspace,刚开始的时候眼挫,忽略了后面的Metaspace,只看到了OutOfMemoryError,就各种找代码问题。 https://arthas.aliyun.com/doc/install-detail.html 然后发现

    2023年04月24日
    浏览(51)
  • 记一次etcd全局锁使用不当导致的事故

    前两天,现场的同事使用开发的程序测试时,发现日志中报 etcdserver: mvcc: database space exceeded ,导致 etcd 无法连接。很奇怪,我们开发的程序只用到了 etcd 做程序的主备,并没有往 etcd 中写入大量的数据,为什么会造成 etcd 空间不足呢?赶紧叫现场的同事查了下 etcd 存储数据的

    2024年02月11日
    浏览(43)
  • 记一次swoole连接数太多导致的错误

    原先就有点担心这个项目正式上线会出现各种问题,所以刚上线就赶紧查看日志 果然,频繁出现错误: WARNING Server::accept_connection(): accept() failed, Error: Too many open files[24] 这个错误通常是由于操作系统限制了进程能够打开的文件句柄数量,导致当前进程无法打开更多的文件,从

    2024年02月02日
    浏览(48)
  • 【现网】记一次并发冲突导致流量放大的生产问题

    目录 事故现象 转账 业务背景介绍 背景一:转账流程 转账流程 转账异常处理 转账异常处理流程图 背景二:账户系统合并 实际全流程: 背景三:扣内存数据库逻辑 背景四:调用方重试逻辑 问题定位 总结  资料获取方法 生产环境,转账相关请求失败量暴增。 直接原因 现网

    2024年02月14日
    浏览(46)
  • 记一次 .NET某工控 宇宙射线 导致程序崩溃分析

    为什么要提 宇宙射线 , 太阳耀斑 导致的程序崩溃呢?主要是昨天在知乎上看了这篇文章:莫非我遇到了传说中的bug? ,由于 rip 中的0x41变成了0x61出现了bit位翻转导致程序崩溃,截图如下: 下面的评论大多是说由于 宇宙射线 ,这个太玄乎了,说实话看到这个 传说bug 的提法

    2024年02月04日
    浏览(43)
  • 【单元测试】Mockito使用详解

    一个单元测试应该有如下特点: 应该是自动化的 应该可以快速运行 每个单元测试不应该依赖其它测试的结果和执行顺序,单元测试框架可以按任意的顺序执行每个测试 每个单元测试不应该依赖数据库,外部文件,或者任何长时间运行的任务。单元测试应该是独立的,不应该

    2024年04月15日
    浏览(86)
  • 单元测试与Mockito

    系列文章目录和关于我 最近在新公司第一次上手写代码,写了一个不是很难的业务逻辑代码,但是在我写单元测试的时候,发现自己对单元测试的理解的就是一坨,整个过程写得慢,还写得臭。造成这种局面我认为是因为: 对Mockito api是不是很熟悉 没有自己单元测试方法论

    2023年04月22日
    浏览(41)
  • Mockito单元测试异常情况

    一、空指针异常 例子: 使用Collectors.groupingBy()进行分组时, 分组值存在null值 。 解决办法:分组值为null时,默认值为空字符,或者让数据不为空。 二、多个参数匹配异常 例如: 如果a和b一个是正常传参(list、map、string…)另一个是任意万能传参(any()、anyList()、anyString(

    2024年02月07日
    浏览(50)
  • Mockito单元测试详解

    依赖: SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以了 (1)@RunWith 指定运行环境,例: @RunWith(SpringRunner.class) Junit运行Spring的测试环境 @RunWith(MockitoJUnitRunner.class) Junit运行Mockito的运行环境,不会加载springboot上下文 @SpringBootTest 加载springbo

    2023年04月09日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包