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

AppetizerIO · 2018年08月27日 · 最后由 AppetizerIO 回复于 2018年09月28日 · 6200 次阅读

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

各种覆盖率

覆盖率,大致就是跑一个用例,覆盖到的代码部分,按照粒度不同可以有 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 条回复 时间 点赞
heygrl 回复

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

AppetizerIO 回复

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

heygrl 回复

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

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

AppetizerIO 回复

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

米阳MeYoung 回复

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

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

徐汪成 回复

可以

仅楼主可见
恒温 回复

其实看完,又更不懂了

龙叔叔

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册