起因是群里有人问增量覆盖率计算,想了一想还是值得写一下。非学究,科普,凑合看吧
覆盖率,大致就是跑一个用例,覆盖到的代码部分,按照粒度不同可以有 Activity 级别/Fragment 级别/H5 页面级别/模块覆盖率/文件级别/类级别/方法级别/行级别,前面三种是安卓特色。最细粒度的是代码行覆盖率,其他的都可以从这个覆盖情况计算出来。
看代码
print("haha");
if (x>0)
print("x>0");
print("haha again");
else
print("x<=0");
print("dada");
引入概念,基本块(Basic Block,简称 BB),表示一段连续的指令,如果执行了第一条,就要执行到最后一条。这段代码有四个 BB
print("haha"); // BB0
if (x>0)
print("x>0"); // BB1
print("haha again"); // BB1
else
print("x<=0"); // BB2
print("dada"); // BB3
计算一次执行(测试用例)会覆盖到哪些行,简单来说只要记录哪些 BB 会被执行到,插入代码(学名插桩)
g_coverage[0] = true;
print("haha"); // BB0
if (x>0)
g_coverage[1] = true;
print("x>0"); // BB1
print("haha again"); // BB1
else
g_coverage[2] = true;
print("x<=0"); // BB2
g_coverage[3] = true;
print("dada"); // BB3
简单易懂,有了 g_coverage
知道一次执行是否会覆盖到每一个 BB,从而能计算出每一行代码是否覆盖。而所谓的方法、类、文件等等覆盖率也都是再二次计算出来的;值得一提的是 if 这句,如果它的两个分支都被覆盖,则 if 被全覆盖;如果只有一个分支被覆盖,叫半覆盖;都没有覆盖,当然就是没覆盖;还有一些特殊条件,比如异常 try catch 这些,看书,简单来说可抛出异常的指令会打断一个 BB
Q: 精细的覆盖率统计真的比粗粒度的覆盖率统计好么?
A: 未必,越精细意味着:1. 统计耗时越大 2. 牵涉到高覆盖率到底有什么意义(见下)
这是一个经典疑问(误解),看代码
if (x > 0)
y = 5 / x; // 没有除零错误
else
y = 5 / (x + 1); // x==-1除零错误
显然两个 x 输入(测试用例)即可完全覆盖,x==2 和 x==-2,但是对找到 x==-1 除零错误没有卵用,以小见大,结论是:
所以全覆盖只是 baseline 而已,一味追求全覆盖并不意味着搞出了有用的测试用例
Jacoco 是开源的 Android/Java 统计代码行覆盖率的主要工具,EclEmma 的替代(记忆里是同一个团队做的)
Jacoco 在 Java 字节码基础上识别出所有基本块,并编号,然后在每个基本块开头插入类似 g_coverage[bb_id] = 1,跑完把 g_coverage 数组存下来,叫 ec 文件,核心大概就是这样,其他的都比较直观不多讲了(原理说来简单,正确实现起来却异常困难,工匠精神)。
因为只要进入了基本快,就一定会执行完,所以只要在开头即标记该基本块整个都被覆盖了即可。
执行完之后,分析 ec 文件,把基本块编号反向映射回代码行数即可展示覆盖情况,也就是每一行 0,1,0.5(分支半覆盖情况)。Jacoco 是行覆盖,类、文件等都是计算出来的
再细节的搜呗;Jacoco 怎么接入?一搜一大把
html 报告效果:行覆盖(绿色),未覆盖(红色),半覆盖(黄色),无视(白色)
从行覆盖计算出来的文件覆盖等。翻译一下,列依次是,未覆盖的字节码行数(指令);未覆盖的分支数 ;圈复杂度(cyclomatic complexity 程序分析概念,不知道也罢);行;方法;类
注意:Jacoco 从 ec 文件产生 html 报告时是需要源代码的,因为 html 报告要呈现源代码的样子
没有具体定义吧,大抵就是一个需求想要知道同一个测试用例在修改前后代码上的行覆盖情况。
假设我们代码的行 diff 是这样的(git diff --no-index a.txt b.txt
):
if (x > 0)
+ print('haha');
- print('dada');
else
+ print('zhazha');
- print('ken');
每一行有一个 diff 状态, t=+/-/没变,三种
分别在修改前后修改后的代码上跑同一个测试用例,每一行有两套覆盖/未覆盖数据。
所以"增量覆盖率"总共有 t*c=12 种情况,实际上比较重要的是:新增的代码没有覆盖,新增的代码被覆盖了,没变的代码原来覆盖了现在没被覆盖;
简单来说就是删掉的代码非亲儿子,新代码没覆盖就是用例需努力,被覆盖了就是用例好样的,没变的代码原来覆盖了现在没被覆盖是退化;
可以通过在改前 改后两个 APK 上跑,获取每一行的覆盖情况,然后在 diff 的基础上加以计算这几种情况。实际实现上,jacoco 有个命令行工具,可以产生 csv 格式的 report,然后(可能)也就是一个 python 脚本的问题
Q: 没变的代码一定按照原来的覆盖情况么?
A: 不是,为啥想一想呗,改了的代码可能会影响后面的执行
Q: 有计算这个的具体代码么?
A: 没有,呵呵,至少我不知道
Q: 那我自己实现容易么?
A: 说起来是挺简单的,但是要注意 diff 每行,有两个行号,一个这行在修改前代码里面的行号,另一个是修改后的代码行号,所以,欢迎贡献
Q: Jacoco 的 debug/release 包都可用么?
A: 我记得app-debug.apk
打开了 Instant Run 之后默认是要关闭 Jacoco 的,所以增量部署的 debug 包上不能用,主要用在 app-release.apk
上面
Q:(评论区)