转转App代码覆盖率方案
作者|张志阳
代码覆盖率是业内常用的统计代码被执行程度的手段。覆盖率数据结果是对测试工作的一种保证,可以赢得信任、增加上线信心。今天就让我们来聊聊 转转App代码覆盖率方案的实现及应用,希望可以给大家带来一些思路和参考。
目的
在转转客户端团队,搭建代码覆盖率方案的主要目的:
通过代码被执行的比例程度,表现测试工作的覆盖程度
通过未覆盖内容的分析&补充测试,保证测试覆盖程度
通过分析无法覆盖的内容,判断代码设计的合理性
这里需要补充强调一点:覆盖率只是一种度量测试完整性的手段, 是一种测试有效性的度量,代码覆盖率100%也并不代表不会有其他的问题。
方案选择
当我们想实现一套专项测试方案的时候,首先都会想到“在业内有没有公开的、流行的、可用的方案,可供参考或者 直接可以”拿来主义”。
首先,我们可以在搜索引擎、论坛上去进行搜索 “App代码覆盖率”, 结果发现每种语言都有自己的覆盖率数据收集方式。
比如现在服务端代码覆盖率使用的Jacoco,是一套非常成熟的Java 方案。
python 、C++、OC 也都有自己体系的覆盖率收集方案,但并没有Jacoco 那么方便集成。更没有已经将多种语言的覆盖率统计集成在一起的方案。
在调研&对比几种方案后,都会发现了一些缺点、功能缺失 和不方便使用的地方,所以开始考虑,能否从0实现一套完全适用自己团队的代码覆盖率方案。
方案构思
在常见的代码结构中,“行”是代码工程的最小单元,所以想要实现代码覆盖率方案来统计代码的被覆盖程度,首先我们应该都会想到,我们需要统计所有“代码行”的覆盖情况,“行”应该是最小的统计单元。
在大家日常的测试工作中,应该经常会做 埋点测试,简单的说,就是通过埋点代码记录某个页面入口/操作/事件 的触发 或者结果的返回,当页面入口/操作/事件 被触发后,记录&上报,埋点系统统计计数,进行业务层次的数据分析。
这和我们当前要实现的代码覆盖率方案需求有些类似,我们想要在代码行被执行后,进行记录&上报,覆盖率系统进行统计,进行代码层次的数据分析,与埋点对比,代码覆盖程度更广一些,但是实现的方式是比较类似的,就是在需要关注的 代码/事件后增加 用于“记录”的代码块,这种添加代码的方式,就是常说的“插桩”(* 基本的原则:保证被测程序原有逻辑的完整性),插入的代码块一般称之为“探针”。
所以想要实现一套代码覆盖率方案的基础就是:
在保证被测App原有代码逻辑的完整性的前提下,通过代码插桩的方式,在每行源代码后插入“探针”代码,记录对应代码行已被执行
将记录的代码覆盖数据上报给覆盖率服务
覆盖率服务 将数据进行保存 & 合并计算
结合团队内的实际方案需求,我们需要先实现以下部分:
实现Android、iOS工程的代码插桩
探针代码被执行时 记录对应代码的执行状态
为了统一Android 、iOS 覆盖率数据的计算及使用方式,需要统一记录时使用的数据结构
代码执行状态数据上报、接收、存储
数据合并计算、存储
实现
1、插桩:
(1)需要解决的难点
难点1: 准确插桩,保证被测App原有代码逻辑的完整性
在日常看到的代码中我们发现,不同的语言,会对代码格式/语法都有不同的要求/规范,每个人在写代码时的习惯也有所不同
在不对代码内容进行语义分析就进行随意插桩,可能会导致编译失败/ 影响原有代码逻辑
语义分析需要对 代码格式/语法 有足够的掌握程度 & 大量的尝试保证语义分析的足够全面
难点2: 所有代码行都进行插桩,会不会影响客户端性能
答案是必然的,当前客户端的代码量,至少已经是百万级别了
如果在每行代码前后插入探针代码、在App实际运行过程中、频繁的IO运算工作,App的 稳定性、性能都不敢保证
(2)解题方案
准确插桩,保证被测App原有代码逻辑的完整性
由客户端RD同学负责插桩方案的具体实现,即高效又可靠
Android: 使用 ASM 对字节码进行分析 ,再进行准确插桩
iOS: 通过语义分析,判断插桩位置,再进行准确插桩
所有代码行都进行插桩,会不会影响客户端性能
初期方案,我们选择先对逻辑分支进行插桩,大量减少插桩量
优点:控制了插桩数量,减少了对工程性能的影响
弱点:只能采集到进入分支,不能判断分支结束;不能按行统计计算, 不能结合Code Diff 直接判断 新增/修改代码的覆盖情况
(3)插桩前后代码对比
Android
iOS
(4)探针代码解读
标记逻辑分支被执行(逻辑分支全局编号)
全局编号从哪来?
在遍历所有类文件时,对识别出的逻辑分支进行编号
插入位置怎么获得
Android:字节码中有行号的描述,通过ASM解析可以获取
iOS:代码遍历,判断出逻辑分支时,就已经知道当前行行号
(5)插桩流程
获取方法路径和方法签名(用于多个tag之间逻辑块执行数据的比较和合并)
方法路径:类名+方法名可以确定当前工程中一个唯一的方法:
com.wuba.zhuanzhuan.activity.AboutZhuanzhuanActivityonCreate(Landroid/os/Bundle;)
方法签名:对方法内的所有字节码做MD5编码,如果方法逻辑有修改(增减),那么签名就会变化
Tag间对比:如果相同方法路径的签名有变化,则认为该方法内的所有逻辑分支都需要重新覆盖测试
找到逻辑块
获取逻辑块行号
逻辑块编号
将方法路径、方法签名、逻辑块信息存入methodMapping文件
插桩结束后,将methodMapping文件上传到服务器
2、记录代码被执行:
(1)数据存储:为了不影响原始代码的执行效率,探针代码执行记录数据的读写效率必须得到保证,所以我们的选择是:
Android:使用字节数组(Bitset) 存储 探针代码执行记录数据
iOS : 在内存中申请指定长度的数组空间 存储 探针代码执行记录
数组长度?
逻辑块编号完成后,可以知道逻辑块总数,一个字节可以存储8个逻辑块编号,即可计算出需要的数组长度
大约会占多少空间?
EXP: 20W 逻辑块 = 2W5 (20W / 8) 字节 = 24.4KB (2W5 / 1024)
(2)探针代码被执行:
探针代码中,入参就是 逻辑块 在所有逻辑块的中编号
将数组中编号对应的位 置为 1 , 代表已覆盖
3、数据上报、接收、存储:
(1)什么时候上报数据
如果频繁上报,可能会影响App的正常使用,也并没有必要。所以考虑在一些 察觉不到/不太Care的 操作节点进行数据上报。
Android:页面的创建、不可见、销毁 时 上报
iOS:App 启动、退后台、唤醒 时上报
(2)上报哪些内容
project: 与 代码覆盖率服务约定的唯一 覆盖率项目名,区分终端,如zhuanzhuanAndroid / zhuanzhuanIos
version: 版本号
uniqueId: methodMapping 文件上传时时间戳,文件的唯一标识
record: 数组数据
(3)数据存储
服务端接收数据后,根据project、version、uniqueId,查询出记录的 record数据,与上报的record 数据 做按位或计算,即 只要有1出现,该位即为1,已覆盖,再将结果存储起来
所以覆盖率数据都是按 uniqueId 进行区分存储的,即每个安装包的覆盖率数据都单独存储。
4、数据合并计算:
(1)两个安装包的覆盖率数据如何合并
数据库中存储的覆盖率数据是每个安装包的数据以及基础信息,当两个安装包并不是同一个Tag,有代码差异的时候,是不能直接进行按位或计算合并数据的。
这时 methodMapping文件就起到了作用。查找到两个安装包对应的methodMapping文件,对其中的内容进行解析,就可以分别获得两个安装包中的所有类名、方法名、方法签名等信息. 通过循环对比,即可进行数据合并:
为了区分两个不同Tag的安装包,方便后续描述的理解,这里将Tag创建时间较早的安装包称之为Old, 将Tag创建时间较晚的安装包称之为New.
遍历New 的Mapping数据
如果Old 的Mapping数据中 有相同的方法签名,则方法内部的所有逻辑分支进行被执行状态的合并计算
如果Old 的Mapping数据中没有相同的方法签名,则认为该方法为新增/修改过的方法,被执行状态以 New 的覆盖率数据为准
最终会得到以New 的代码为准的覆盖率数据,即相对较新代码的整体覆盖率数据
(2)要合并哪些安装包
我们存储了那么多安装包的覆盖率数据,那么在实际的客户端迭代流程中,我们应该合并哪些安装包的数据,才可以帮助我们进行分析,实现我们的方案目的呢?
我们对 单次打包纬度、单Tag纬度、版本纬度、分支纬度 几种统计纬度进行了利弊分析&对比,最后我们选择使用分支纬度进行覆盖率数据的汇总统计,即根据Tag的前后节点关系,合并同一分支线上的所有Tag(以前一版本的发版Tag为起点,以当前分支线中最新的Tag为终点)的所有安装包的覆盖率数据。
(3)多个安装包的数据集如何快速合并
为了保证合并计算效率,采用多线程来处理。
以前一版本的发版Tag为起点,以当前分支线中最新的Tag为终点,根据记录的Tag前后节点关系,查询出分支线中的Tag列表,再查询出对应的所有安装包覆盖率数据集
多线程分别合并数据集
流程
1、简化流程
2、打包流程
3、测试过程中上报流程
4、自动计算流程
平台功能
1、查看指定版本、分支的增量代码覆盖率数据 & 依赖组件工程的增量代码覆盖率数据
2、查看组件工程中详细的类增量覆盖率数据
3、查看类增量代码中逻辑分支的详细覆盖状态 & 实时计算、渲染
4、选择Tag区间,临时计算增量覆盖率数据
5、选择同版本两个Tag ,对比计算增量覆盖率数据
后续规划
分支统计纬度的优缺点前面已经提到过,接下来我们会增加行覆盖和方法覆盖统计纬度,并且会结合Git diff,计算/渲染 Diff 代码的覆盖率数据。
现在iOS的插桩方案有些简单粗暴,效率也并不高,所以已经开始着手重构。
我们也计划在平台上增加更多的人性化的功能,提升覆盖率数据的分析效率、提升整体的使用体验。
好了,转转App代码覆盖率方案 就先给大家介绍的这里,希望能对大家有所帮助。
如果喜欢我们分享的内容,欢迎点赞、在看、分享~