精准测试系列分享之一:JaCoCo 企业级应用的优缺点分析
一、JaCoCo简介
JaCoCo是Eclipse平台下的开源产品,以小型,轻量化著称,常见集成在Eclipse Workbench中,除此之外的启动方式包括对接Ant和Maven,或是命令行的方式进行。Jacoco近两年在软件测试行业的被关注度比较高,其主要原因是:在新一代精准测试技术流的影响中,各大型单位对覆盖率的追求越来越迫切。作为一款开源产品,它主机面向Java语言,能够在字节码层面给出覆盖率,同时也能将字节码关联到对应的源代码。这种半精准的测试方式,在小型团队中,对于某些场景的覆盖率诉求,起到了一定的响应。但它也有很强的局限性,尤其在支撑大型系统应用中,其表现能力弱,准确率不够达标。这是它致命的缺点,也为它在大型系统中应用的 失败埋下了伏笔。如何下此结论?本文作者将从应用者的角度,并参照国外用户的总结文献做一综合分析罗列。因本人的学识所限,归纳的肯定不够全面,如有不准确之处,还请同行拍砖、指正。
二、JaCoCo的技术内核
JaCoCo覆盖率的采集,主要是通过插装及装点的执行来收集程序运行的特征数据。JaCoCo的插装,是通过agent在字节码中插入boolean[]数组,来标记每句可执行代码,只要执行过相应语句,boolean[]数组会产生相应标记(True or False),这个boolean数组连同产生的标记称之为探针(Probe)。
当源码被编译为字节码时,源码的逻辑指令,例如条件判定,循环等,会被编译为特殊指令,JaCoCo在动态插装的过程中,当处理到这些指令时,会根据不同的逻辑进行不同的插装方式。
图表 1 Java源代码(左)和对应字节码 (右)
图1中IFEQ和G0T0都是JUMP(跳转)指令,通过JUMP指令直接能从一个节点,跳转到另一个节点。针对JUMP指令的插装方式,有多种选择。有些是在指令前插入探针,有些是在指令后插入探针(见表格1)。类似的,在面对其他特殊指令时,例如ENTRY(程序启动), SEQUENCE(序列执行),或者EXIT(程序终止)等,均有不同的插装操作。图2以面对JUMP类型指令的插装作为示例,其中左图为未插装的字节码,右图为装点位置的示意图。
图表 2针对JUMP类型指令的插装示意
表格 1面对不同类型指令的插装方式
JaCoCo的这种插装方式,特别需要指出的是:
- 看似针对不同类型的指令,通过不同的插装方式,反映了不同的逻辑关系,但由于其对整体的结构没有把控,当程序较为复杂时,十分容易产生误差,并且难以反映真实的调用关系。
- 除了表1列出的几种指令外,JaCoCo对于其他的指令视为Exception,并不予处理,整个插装过程,以及插装位置对于用户而言均不可见,无法通过人工干预合理和调整插装问题,导致即使用户有排查问题的需求和能力,也无从有效研究问题的根源。
- 这种无序的混乱插装方式,也造成不必要的性能开销,比如:使用JaCoCo会使代码的膨胀率增加约30%,并增加约10%的性能消耗。(由于JaCoCo的设计初衷是针对民用项目,因此优化程度较低,当项目规模较小时,其影响并不显著,但是对于资源十分宝贵的商业项目,会产生较大的负荷。)
三、JaCoCo的覆盖率实现
比较常见的覆盖率标准,按粒度从粗到细依次是:语句覆盖,分支覆盖,条件覆盖,路径覆盖。JaCoCo可以实现语句覆盖,以及部分情况下的分支覆盖。一些研究表明,JaCoCo在分支覆盖层级,对结构控制只研究了if-else的情况,因此该层级的评估无法保证所有可执行语句的正确执行[1]。同时,JaCoCo所谓支持的条件覆盖结果其实不适合商用,由于字节码和源码之间的信息差,它只能衡量条件真假是否覆盖的比率,但无法有效确定某个具体条件执行的精确结果。而在企业级应用场景下,实现精确的条件覆盖是刚性需求,JaCoCo因为没有代码静态分析辅助永远无法纯粹通过字节码获取精确的条件和分支覆盖。
因此,JaCoCo优点是,可以进行粗粒度的覆盖率度量,比较适用于对于项目的整体初步评估。
在真正的精准测试的体系中,对于覆盖率的计算和展示,其实现基础是测试信息到源码,再从源码到测试用例的精准关联和信息映射。与其相比,Jacoco最大的弱点是:易产生覆盖率的计算偏差。
- JaCoCo原生不支持和测试用例的关联。因此只能在手动执行测试用例后,给出当前测试用例所对应的整体覆盖率信息。受限于设计理念,JaCoCo的覆盖率是在字节码层级统计,为了用户的可读性和可研究性,需要和源码进行关联,关联的过程通过一种映射机制(mapping)来实现,而在这种映射的过程中,会损失大量的信息,因此,会进一步造成覆盖率信息的误差。
- 在覆盖率的应用端,商业适配性较差。第一,JaCoCo不支持多项目并行或者多版本累计的覆盖率统计,其覆盖率只针对某个用例对于某个项目的评估。JaCoCo的理念更偏向于传统白盒测试工具,在敏捷迭代的场景下,JaCoCo无法对版本/轮次/里程碑版本/项目之间的差异或者总体覆盖率计算进行支持。第二,JaCoCo原生不支持真正意义上的精准测试,其内部数据表达都是聚合的数组,不支持区分不同线程的覆盖率统计。第三,JaCoCo不支持实时的覆盖率展示,其覆盖率结果只能在该测试任务结束后生成。第四,JaCoCo不支持结果的自动保存和累计,一旦退出环境,所有未经保存的结果都会被清空。
- 对于程序的逻辑结构和调用关系,JaCoCo除了在动态插装的过程中进行实时分析以外,并无其他的获取方式。而对于覆盖率的JaCoCo能够在Java文件的基础结构层级的基础上,从四类数据:覆盖率,被覆盖行数,未覆盖行数,以及总行数,实现覆盖程度的评估和展示。其界面如图3所示。同时,在实现源码和字节码的映射后,统计的覆盖率信息也会相应的在源码端的代码行上进行展示,界面如图4所示,其中绿色代表完全覆盖,黄色代表部分覆盖(一些指令或者分支未覆盖),红色代表未覆盖,这些颜色也支持用户自定义。针对覆盖结果的导出,JaCoCo支持HTML,XML,CSV等文件格式,用户也可以在Eclipse内进行查看。
图表 3 JaCoCo覆盖率展示界面
图表 4 JaCoCo在源码端的覆盖率展示
四、应用局限性
JaCoCo天生缺陷的内核设计和定位,带来了应用的局限。作为一款热门开源工具,虽然被爱好者们在小范围及初步信息判断中使用较多,但是却因为大量的误差,不能被应用更大、更复杂的大型核心系统上,否则用户可能将面临着很大的技术风险。
覆盖率误差:JaCoCo的插装过程具有一定的技术局限性,其装点并非明文可见,存在大量的信息差。发表在IEEE上的文章:“Negative effects of bytecode instrumentation on Java source code coverage”,对JaCoCo的误差情况以及可能原因进行了详尽的评估与分析[2]。
JaCoCo与标准基线对比结果显示:针对不同的项目,在类(Method)层级,JaCoCo的覆盖率评估就已产生了0.2%-8.5%左右的绝对误差和0.7%-23.859%的相对误差。在更细化的语句行和分支层级,其误差将会被指数级放大。
表格 2 JaCoCo覆盖率评估的相对误差[2]
误差产生的具体成因:
- 复杂系统通常由大量子模块组成,JaCoCo无法实现对于内部被调用的子模块进行插装,因此对于子模块覆盖率的评估会产生显著的误差。
- 如果某个子模块没有被调用,那么对于JaCoCo来说,该模块内的方法等同于不存在。JaCoCo需要调用该子模块,才能将该子模块内的代码计入覆盖率计算的“分母”。
- 除了几种既定的逻辑意外事件,JaCoCo无法正确处理例外情况(Exception),如果在控制流程中遇到Exception,JaCoCo会把这种情况直接标记为未覆盖,这种判定方式直接的影响到了对程序逻辑关系的把控,造成对于覆盖率无法准确评估。
误差引发的后果:
- 伪瓶颈的产生,以及对测试质量的错误高估。第一种情况,测试人员投入大量工作之后,却无法进一步提升覆盖率,造成对资源和实践的浪费;第二种情况,会让用户误将未达标的系统判定为达标,有可能引发严重的生产事故。
- 无法实现缺陷定位,大量的算法和应用依托覆盖率的输入,而缺陷定位更是其中最主要的实践。
- 回归测试的精准度,受到了严重的影响。
五、对接能力分析
JaCoCo对于Ant和Maven有较完整的插件支持启动方案,但是只能和Eclipse或者SonarCube集成,无法实现和测试管理软件,或是上下游测试工具的完整对接。
对于开发流程,JaCoCo依然是传统白盒测试的思维,即瀑布式的开发模式,需要在代码更新后重新进行测试,每次版本迭代的工作量都十分庞大。
自动化层面,JaCoCo支持与Jenkins的对接。
六、结语
JaCoCo能够有效的在字节码层级提供覆盖率的初步评估和计算,并且有实现从字节码关联到源码的能力。但是,JaCoCo只是一款提供粗略评估工具,无法自动关联用例、无法有效提升测试效率,没有作为测试中台的对接和支持能力。JaCoCo的定位和实践,表明其更适用于偏向辅助个人开发者和小型项目组对项目覆盖率进行非常基础的评估,对于支撑大型企业级应用的精准测试需求,依然路途漫长。
参考文献:
[1] N. Li, X. Meng, J. Offutt, and L. Deng, “Is bytecode instrumentation as good as source code instrumentation: An empirical study with industrial tools (Experience Report),” 2013 IEEE 24th Int. Symp. Softw. Reliab. Eng. ISSRE 2013, pp. 380–389, 2013, doi: 10.1109/ISSRE.2013.6698891.
[2] D. Tengeri, F. Horváth, Á. Beszédes, T. Gergely, and T. Gyimóthy, “Negative effects of bytecode instrumentation on Java source code coverage,” 2016 IEEE 23rd Int. Conf. Softw. Anal. Evol. Reengineering, SANER 2016, vol. 1, pp. 225–235, 2016, doi: 10.1109/SANER.2016.61.