起点客户端精准化测试的演进之路
一
背景
代码重构了,我也不知道影响什么业务……
我就升级了SDK,不知道有什么影响……
代码改动挺多的,要么全测一遍吧!
我就改了一行代码,你要测几天?
这就是当前敏捷开发模式下,测试和开发的矛盾会越来越多的原因。快速的迭代,会极大的扩大测试的回归成本。
例如一个线上问题,对指定机型的设备有bug,开发进行修复后,对指定机型进行了if else判断,测试用例可以缩减到当前判断分别为true和false的场景。
那么在敏捷迭代的这样一个环境下,开发者和测试怎么来保证,我「提交的代码」、「测过的Case」在任何时候都是正确的呢?
当你无法量化的时候,你就在用你的人品和信誉做担保,而开发团队对你的信任也是基于你的信誉。可是在你还没有建立你的信誉的时候,你就必须拿出量化的东西来赢得信任。
精准化测试,需要测试从提交的代码中找到具体的业务修改点,这对测试的要求很高,他需要从开发提交的代码中了解具体修改的业务逻辑点,而开发的一个commit,有时候并不是很纯粹,经常会夹带一些「私货」,这也是引起测试未覆盖的一个重要原因。
但是,精准化测试并不是测试质量保证体系的银弹,对于精准化的覆盖率测试,需要先明确一个前提公理,那就是Coverage覆盖到的代码,就算是testcase执行完成了。因为覆盖率测试只能保障代码被执行,而执行逻辑是否正确,并不在保证之内。
因此,盲目的追求代码覆盖率是没有意义的,即使已经达到了 100%的代码覆盖率,软件的质量也不可能做到万无一失,因为代码覆盖率的计算只是基于执行代码的,并不能发现那些「未考虑某些输入」、「未判断的逻辑异常」以及「未处理某些情况」形成的缺陷。
二
设计思路
针对上面的这些问题,在开发的角度,我们希望能对测试的效率进行提升。
JaCoco默认场景下是不支持增量插入探针的,但在实际开发过程中,一般不太会对全量代码做检测,所以,我们需要改造JaCoco,为精准化测试平台提供增量探针功能。同时,还需要接入CI系统,与现有软件开发流程对接,完成「开发-测试-回归」的闭环。
大部分公司的CI系统都会采用Jenkins来搭建,起点在接入腾讯的蓝盾之前,也是采用的Jenkins,所以,整体的系统流程图如下所示。
至此,我们就完成了从开发到测试,并支持完整CI流程的方案设计。
由于JaCoco Android Plugin在客户端没有增量的实现,所以,我们需要单独实现一套Plugin系统,在借助JaCoco探针的插入机制基础上,完成对增量代码的探针修改。
通常在增量修改方案有以下三种:
增量方案1:在插桩时,对diff信息做增量,精确到行级别插桩
增量方案2:插桩时做全量插桩,在生成报告时对diff信息做过滤,在报告级别做行增量
增量方案3:插桩时对Class级别做diff过滤,并进行Class内的全量插桩,在生成报告时,针对diff信息的行级别做报告的过滤
这里我们选择的第三种方案来实现。这是一种性价比最高的方案,通过获取diff信息,在插件层不用做太多的修改,只需要过滤掉非diff的文件,而在生成报告时,再借助diff行号来做高亮展示即可。
在Gradle插件中,我们在Transform过程中来获取增量内容,借助Git diff的执行结果,过滤需要做增量的修改Class文件,相关代码如下所示。
增量探针的问题解决了,那么下面的问题就是如何获取diff信息了。在Git中,我们可以很方便的通过git diff命令来获取增量信息。
该命令格式如下所示,它用来对比不同commit(或分支)间的增量代码:git diff [<options>] <commit> <commit>
其中commit可以是分支名,也可以是commit的id,对比分支间的差异,可以简写为 git diff targetBranchName,表示对比当前分支与目标分支间的代码差异。diff文件的解析这里不做过多的解释,感兴趣的朋友可以参考我的博客 https://xuyisheng.top/diff/
获取到diff增量信息之后,就可以借助正则来获取我们需要的信息了——diff文件,以及该文件中的diff行号。
一般来说,修改JaCoco的Report生成逻辑,通常是通过修改JaCoco Analyzer类的analyzeClass函数。但实际上,JaCoco的Report库,给我们提供了修改的接口。在修改之前,我们先来了解下JaCoco默认的Report机制。
JaCoco覆盖率报告分为三层递进数据:Package——Class——Method,其覆盖率统计实际上是由内向外的。先了解下场景,我们修改了两行代码,并执行了其中一行。我们先看Method级别:
这里由于是增量代码检测,所以在MainActivity里面的所有代码中,只有有代码修改的两个方法有覆盖率数据,即test2和test3。其中test2执行了,所以其覆盖率为100%,而test3未执行,所以其覆盖率数据未0%。再来看看Class级别:
在前面的插桩过程中,实际上我们已经拿到了「修改的文件」——「修改的具体行号」这样的一个对应关系文件,只要我们再读出JaCoco覆盖率文件中,这些修改的行,是否被执行的数据,其实就可以完成覆盖率的统计了,我们简单的定义「增量覆盖率」的统计规则:
该文件中被修改的代码中已执行或者部分执行的代码行数 / 该文件中被修改的所有代码行数 %
有了这些数据之后,我们也不需要再使用JaCoco的覆盖率报表了,直接使用自己的统计数据,而只需要最后跳转每个文件的源代码覆盖文件即可。
那么现在的问题就剩下如何拿到行是否被执行的覆盖率数据了。核心代码如下所示。
JaCoco的原生报表,结构比较复杂,不适合展示和汇总,特别是不利于非技术人员的使用,所以,我们需要对原生报表进行修改,让报表更加直观。
所以,在梳理了覆盖率真正的需求之后,我们抽象出了新的覆盖率报表的必须项目:
文件名
已覆盖了多少行
总共修改了多少行
提交信息
覆盖率数据
这是汇总表,针对每个文件的详情,我们可以继续使用JaCoco的明细报表。在官方Demo中,我们发现,报表数据实际上都被解析到了IBundleCoverage中,所以,我们只需要遍历IBundleCoverage数据,就可以拿到JaCoco的统计数据,IBundleCoverage的统计维度和它原生的报表结构类似,最外层是packages数据,内部遍历是sourceFiles数据,再到内部,就能拿到行数据,在行数据中,就可以拿到该行是否被覆盖的状态,OK,顺着这个思路,我们就可以自己来统计相关的覆盖率数据了,在遍历的过程中,我们可以使用一些Map来保存遍历出来的数据,最后输出到文件中,通过自定义的HTML文件展示出来。核心代码如下所示。
这里有几个细节需要注意:
在统计覆盖率时,我们进行了包容处理,即只要不是完全未执行的代码,我们都认为是已执行,这样做的原因主要是因为PARTLY_COVERED在Kotlin中经常会因为一些语法而产生一些无效的标识,而且有些场景下,一个条件分支的部分执行,就已经完成了逻辑的处理,所以,这样的计算也是合理的
上面这张图就是最终生成的覆盖率报告,点击相关类,就可以跳转到相关的类详细覆盖率文件界面,这里是复用的JaCoco覆盖率展示页面,这里不再赘述。
三
精准化测试的作用
精准化测试对开发和测试的收益如下:
将黑盒测试转化为白盒测试
统计到行,提高了发现问题的精读和效率
提升了测试回归用例的效率
反向约束了代码规范
在精准化测试平台的迭代过程中,通过覆盖率文件的分析,发现了很多在一般测试流程中很难发现的问题,下面简单的列举一些在测试过程中发现的问题。
代码分支未覆盖,该部分逻辑未得到验证
代码冗余,开发可以发现潜在的不规范代码
修改了与提交信息不同的修改,场景不会被测试用例覆盖
银弹?
在开发精准化测试平台的过程中,我们也发现了这个方案的一些不足,这也印证了那句老生常谈的话——软件开发领域没有永恒的银弹。
首先,精准化测试在没有自动化测试平台的基础上,如果测试代码频繁修改,那么会导致增量覆盖率文件存在一些问题。因为精准化测试报告与代码版本是一一对应的关系,这就好比是医院的验血报告,报告只对当次提交的血液检测样本负责,覆盖率报告也是同样的道理。
其次,精准化测试平台在多人协作的场景下有一些不足,覆盖率记录文件在本地设备中,所以多人协同测试的场景下,本地覆盖率文件难以合并。
通过自动化测试,回归已测场景,从而产出迭代的覆盖率报告
在此感谢起点客户端团队和起点测试团队在精准化测试平台的搭建中提供的宝贵建议和帮助
作者介绍