移动测试基础 你需要知道的覆盖率 / 原理 / 解惑 / 增量覆盖率计算

AppetizerIO · August 27, 2018 · Last by AppetizerIO replied at September 28, 2018 · 3568 hits

起因是群里有人问增量覆盖率计算,想了一想还是值得写一下。非学究,科普,凑合看吧

各种覆盖率

覆盖率,大致就是跑一个用例,覆盖到的代码部分,按照粒度不同可以有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除零错误没有卵用,以小见大,结论是:

  • 高覆盖的测试用例 != 测试用例有用
  • 没覆盖的分支 == 该分支上的任何错误肯定都测不到,注意错误不限于Exception

所以全覆盖只是baseline而已,一味追求全覆盖并不意味着搞出了有用的测试用例

Jacoco 统计Java/Android行覆盖率

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: (评论区)

参考

  • 没有,我拍脑袋写的,凑合看吧,硬是要的话去看看jacoco的官方文档吧
  • 要真的融会贯通,看compiler的书吧:龙书/鲸书
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 12 条回复 时间 点赞

龙叔叔

恒温 回复

其实看完,又更不懂了

Author only
徐汪成 回复

可以

覆盖率的统计对测试还是有很大帮助,可以让以往的测试能量化。

米阳MeYoung 回复

是的,从无到有是必要的,因为 没覆盖的分支 == 该分支上的任何错误肯定都测不到,注意错误不限于Exception,要确保大回归有比较高的覆盖率;但是只看覆盖是不够的,覆盖只是基本款

AppetizerIO 回复

嗯。 还有代码覆盖率 和 Diff 系统结合, 通过精准测试辅助系统,做到记录以往case的代码方法的匹配情况,到时代码变更也可以从而推出case。 现在越来越多公司开始再捣鼓精准测试, 精准测试还是离不开代码覆盖。

现在Android项目工程结构组件化以后,统计貌似会出问题

heygrl 回复

主要问题是aar好像不太能插桩,因为jacoco是在 javac的时候,用asm.jar插桩的,一种方法是exclude掉aar的代码,这样统计主干,然后aar自己测自己

AppetizerIO 回复

现在比较蛋疼的是,虽然组件化了,但是代码都在一个git仓,主模块以依赖的形式引入其他模块,测试也是一个完整的app测试,没有单独分开,所以现在就出现问题了,插桩后打完包很大概率直接crash

heygrl 回复

这比较蛋疼 compile (":xxx") 实际build时候流程和gradle层面复杂度太高,真 组件化应该是真的git repo分离,每个子项目有自己的中间产物(jar/aar),自己的unittest,在自己的unittest里面有接入jacoco,有基本的library级别的覆盖率数据,然后在apk层面,总的包含所有aar/jar,然后保证jacoco agent不重复冲突,exclude掉一些类,有一个整app级别的测试用力的覆盖率数据

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up