单测结果不稳定的终极解决方案(Maven单测参数调优)

一、前言

近期,在公司平台执行单测任务时,我发现到一个显著的问题:我们的一个应用,在公司平台上执行单测时,即使是相同的代码,每次的执行结果(包括行覆盖率以及单测通过率)都存在差异。更具体地说,许多在本地环境中能够成功执行的测试用例,在公司平台上却遭遇了失败。

为了解决这个问题,我进行了广泛的信息搜索,咨询了 ChatGPT,并尝试了多种可能的解决方案。经过不懈的努力,我终于找到了问题的根源并成功解决了它。在接下来的内容中,我将与大家分享这个问题的解决过程以及导致问题的根本原因。

二、问题解决过程

1、问题概述

在公司平台执行单元测试过程中,一个应用表现出了显著的测试结果波动性。不仅测试通过率波动较大,而且每次执行的测试用例数量也呈现不一致性。

2、问题解决过程

A、方案1:去掉并行执行

遇到问题时,我们首先考虑的是寻求平台团队的支持。平台团队给出的反馈是,我们的单元测试可能不适合并发执行(这是一个很好的思路)。由于平台默认启用了单元测试的并发执行功能(参数-T 2C)。如果我们希望关闭并发执行,只需删除改参数配置即可(有关该参数的详细说明将在下一节中提供)。

在移除并行调用参数后,我对单元测试进行了多次重新执行,但遗憾的是,结果并未出现预期的改善。

B、方案二:正确使用静态类mock

在上述方案未能取得预期效果后,我继续深入分析单元测试结果。通过对比两次执行结果生成的单元测试报告,我发现执行成功率较低的报告中记录了大量的静态mock错误,这些错误在执行成功率较高的报告中并未出现:static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered。

鉴于我们当前使用的是Mockito 4.X版本,我们采用mockStatic方法来模拟静态类。由于许多团队成员对这种mockStatic的使用方式尚不熟悉(之前习惯使用PowerMock),因此在使用过程中可能会出现不规范的情况。熟悉Mockito.mockStatic方法的同事应该都知道,一旦启用了mockStatic,确保在测试结束后正确关闭它是非常重要的。因此,我们通常建议使用try和finally块来确保mockStatic的开启和关闭,具体操作如下所示。如果没有按照这种方式使用,就可能会遇到前述的静态mock错误。

随后,我着手修正所有的错误用法。在完成这些修正后,我再次在平台上多次执行单元测试用例。最终发现,虽然静态mock的错误得到了解决,但单元测试的结果仍然不够稳定,尽管整体表现有所改善。

C、方案三:禁用fork进程重用

在上面方案未能取得成功后,我继续探索其他可能的解决方案。最终,我转向了Maven的官方文档,寻找关于单元测试并行执行的参数调优信息。在阅读了Maven官方文档中关于并行执行的参数调整部分(https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html)后,我了解到,即使去除了最初方案中的并行参数,Maven默认仍然提供了一种提高单元测试用例执行速度的方式,即通过设置`reuseForks`参数。

默认情况下,Maven会设置reuseForks=true/forkCount=1,这意味着Maven将创建一个进程来执行单元测试,并且所有的单元测试都会通过这个进程运行。这种方式通常没有问题,但是如果单元测试之间使用了公共资源(如上下文、静态变量和静态配置等),则可能会导致进程间相互影响。因此,我将reuseForks设置为false,让Maven为每个单元测试类创建一个新的进程来执行。这样修改后,我连续执行了几次单元测试,发现结果非常稳定,而且整体的单元测试结果也有所提升。

3、根因总结

禁用进程复用功能后,单元测试结果的不稳定问题得到解决,这表明确实存在单元测试用例之间使用公共资源的问题。由于单元测试用例数量庞大,没有足够的时间去逐一分析每个用例中具体存在哪些公共资源的使用问题。

三、Maven单测参数调优

1、默认参数(单进程复用)

设置:默认情况下,`reuseFork`设置为`true`,这意味着Maven会尝试复用已经创建的进程来执行后续的测试用例。

执行方式:所有测试用例会依次在一个进程中串行执行。

可能的问题:如果测试用例之间有依赖关系,可能会导致测试结果不稳定。

2、Module维度并发执行(线程维度)

设置:通过命令行参数`-T 2`或`-T 2C`来设置线程数。`-T 2C`表示线程数等于CPU核心数的两倍。

执行方式:如果有多个模块,每个模块的测试会用一个线程并发执行。

可能的问题:如果模块间的测试用例有资源竞争,可能会导致测试结果不稳定。

3、单测维度并发执行(线程维度)

设置:使用`parallel`参数,可以设置在类维度(`classes`)或方法维度(`methods`)并发执行。还可以设置线程池大小等参数。

执行方式:测试用例会在类或方法维度上并发执行。

可能的问题:如果测试用例之间存在资源竞争,可能会导致测试结果不稳定。

4、Fork进程方式并发执行(进程维度)

设置:通过设置`forkCount=N`和`reuseFork=true`(默认为`true`)来开启多进程并发执行。

执行方式:每个测试用例类会在一个独立的进程中执行。

可能的问题:如果测试用例类之间有资源竞争,可能会导致测试结果不稳定。

在使用这些并发执行方式时,需要注意的是,虽然它们可以显著提高测试的执行效率,但同时也会增加测试用例之间相互干扰的风险。因此,在采用这些并发执行方式之前,应该确保测试用例之间是相互独立的,没有共享资源或状态。

在实际应用中,可能需要结合项目的具体情况和测试用例的特点,选择最合适的并发执行策略。如果测试用例之间存在共享资源的问题,可能需要采取额外的措施,如使用测试数据库、配置文件副本、环境隔离等,以确保测试的稳定性和可靠性。

5、如何选择

根据不同的需求和场景,我们需要在单元测试的并发执行方面做出合理的选择。

A、单元测试支持并发执行

可以同时开启Module维度并发和单测维度并发。这样可以最大化地利用多核CPU的能力,提高测试的执行效率。但是,需要确保测试用例之间没有共享资源,以避免并发执行带来的潜在问题。

B、单元测试不支持并发,但是可以接受单测结果的波动

可以合理调整并发线程数(调小一点),并开启并发。这样做可以平衡执行速度和结果稳定性。线程数越少,潜在的并发冲突和数据竞争就越小,从而减少测试结果的波动。

C、单元测试不支持并发,但是需要单测结果稳定

应该关闭并发执行。在这种情况下,可以只使用默认的单进程复用设置,确保测试用例按顺序串行执行,从而保证测试结果的稳定性。

D、在默认值的基础上,仍然存在不稳定的情况

考虑关闭`reuseFork`。如果测试用例之间存在共享资源,可能会导致测试结果不稳定。关闭`reuseFork`可以确保每个测试用例都在新的进程中执行,从而减少进程间的相互影响。

总的来说,选择哪种并发执行策略取决于项目需求、测试用例的设计以及可接受的测试结果稳定性。从执行速度来看,A选项通常最快,因为它是最大程度地利用了并发执行的优势,而D选项通常最慢,因为它完全避免了进程复用,每个测试用例都在新的进程中执行。在实际应用中,可能需要根据测试结果和项目进度进行多次调整,以达到最佳的测试效果。

四、附录

单测并发执行(Maven官方)

Maven Surefire Plugin – Fork Options and Parallel Test Execution

五、惯例

如果你喜欢本文或觉得本文对你有所帮助,欢迎一键三连支持,非常感谢。
如果你对本文有任何疑问或者高见,欢迎添加公众号lifeofcoder共同交流探讨(添加公众号可以获得楼主最新博文推送以及”Java高级架构“上10G视频和图文资料哦)。

你可能感兴趣的:(技术分享,maven,java,fork,单元测试)