转转App代码覆盖率方案

共 4272字,需浏览 9分钟

 ·

2020-10-16 21:49

作者|张志阳

代码覆盖率是业内常用的统计代码被执行程度的手段。覆盖率数据结果是对测试工作的一种保证,可以赢得信任、增加上线信心。今天就让我们来聊聊 转转App代码覆盖率方案的实现及应用,希望可以给大家带来一些思路和参考。

目的

在转转客户端团队,搭建代码覆盖率方案的主要目的:

  • 通过代码被执行的比例程度,表现测试工作的覆盖程度

  • 通过未覆盖内容的分析&补充测试,保证测试覆盖程度

  • 通过分析无法覆盖的内容,判断代码设计的合理性

这里需要补充强调一点:覆盖率只是一种度量测试完整性的手段, 是一种测试有效性的度量,代码覆盖率100%也并不代表不会有其他的问题。


方案选择

当我们想实现一套专项测试方案的时候,首先都会想到“在业内有没有公开的、流行的、可用的方案,可供参考或者 直接可以”拿来主义”。

首先,我们可以在搜索引擎、论坛上去进行搜索 “App代码覆盖率”, 结果发现每种语言都有自己的覆盖率数据收集方式。

比如现在服务端代码覆盖率使用的Jacoco,是一套非常成熟的Java 方案。

python 、C++、OC 也都有自己体系的覆盖率收集方案,但并没有Jacoco 那么方便集成。更没有已经将多种语言的覆盖率统计集成在一起的方案。


在调研&对比几种方案后,都会发现了一些缺点、功能缺失 和不方便使用的地方,所以开始考虑,能否从0实现一套完全适用自己团队的代码覆盖率方案。


方案构思

在常见的代码结构中,“行”是代码工程的最小单元,所以想要实现代码覆盖率方案来统计代码的被覆盖程度,首先我们应该都会想到,我们需要统计所有“代码行”的覆盖情况,“行”应该是最小的统计单元。


在大家日常的测试工作中,应该经常会做 埋点测试,简单的说,就是通过埋点代码记录某个页面入口/操作/事件 的触发 或者结果的返回,当页面入口/操作/事件 被触发后,记录&上报,埋点系统统计计数,进行业务层次的数据分析。


这和我们当前要实现的代码覆盖率方案需求有些类似,我们想要在代码行被执行后,进行记录&上报,覆盖率系统进行统计,进行代码层次的数据分析,与埋点对比,代码覆盖程度更广一些,但是实现的方式是比较类似的,就是在需要关注的 代码/事件后增加 用于“记录”的代码块,这种添加代码的方式,就是常说的插桩(* 基本的原则:保证被测程序原有逻辑的完整性)插入的代码块一般称之为探针”。

所以想要实现一套代码覆盖率方案基础就是:

  1. 在保证被测App原有代码逻辑的完整性的前提下,通过代码插桩的方式,在每行源代码后插入“探针”代码,记录对应代码行已被执行

  2. 将记录的代码覆盖数据上报给覆盖率服务

  3. 覆盖率服务 将数据进行保存 & 合并计算

结合团队内的实际方案需求我们需要先实现以下部分

  1. 实现Android、iOS工程的代码插桩

  2. 探针代码被执行时 记录对应代码的执行状态

    为了统一Android 、iOS 覆盖率数据的计算及使用方式,需要统一记录时使用的数据结构

  3. 代码执行状态数据上报、接收、存储

  4. 数据合并计算、存储


实现

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代码覆盖率方案 就先给大家介绍的这里,希望能对大家有所帮助。

如果喜欢我们分享的内容,欢迎点赞、在看、分享~






浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报