猿辅导 iOS 精准测试实践 - Objective-C 与 Swift 混编工程精准测试探索

 

背景


猿辅导正处于扩张期,各业务线高速迭代,那么如何在团队/业务规模快速发展的同时保证交付到用户手中的产品质量呢?测试是验证产品质量的重要环节,在当今移动互联网对于 App 版本迭代周期不断缩短的情况下传统黑/白盒测试的表现都不尽人意:

  • 黑盒测试,人工评定的黑盒测试需要大量人力成本投入,测试执行无法精准衡量控制,对 QA 人力经验依赖大,人员变动成本大,质量抖动幅度大。

  • 白盒测试,单元测试为主的白盒测试虽然可以自动化执行并跑出量化数据,不过编写/维护单测的人力成本从 QA 转移到了 RD,编写单测成本高,随业务需求的快速迭代维护成本也不低。

有没有一种测试方案可以在不增加工作量的同时既满足不断缩短的迭代周期要求又能够准确采集并衡量测试过程呢?「精准测试」是我们找到的答案。

索引

  • 什么是「精准测试」?

  • 代码覆盖率检测原理剖析

  • 增量代码覆盖率设计方案

  • 总结与展望

什么是「精准测试」?


 

在回答这个问题前请允许我再提出几个问题:

  • 面对同样的 Feature,不同 QA 通过评估给出的测试用例一致吗?

  • 如果 QA 小 a 给出了 10 个测试用例,小 b 给出了 20 个测试用例该以谁的用例为准?

  • 测试用例越多越好吗?

「精准测试」指通过测试示波器技术,记录黑盒测试用例对应的代码逻辑,实现测试用例到代码逻辑的精准记录和双向追溯;代码级的缺陷定位和崩溃分析;精准的测试充分度分析。

精准测试的概念最近在各大技术垂直社区和开发者大会上多次被提及(举个 🌰 GMTC19 就可以搜到不止一篇关于精准测试的演讲资料),我的理解是通过技术手段记录黑盒测试执行用例时的数据,输出精确到源代码级别的可视化数据来达到核心思想中的用例与代码双向映射精准的测试充分度分析

Emmmmm...至于代码级的缺陷定位/崩溃分析这一点不在本文的讨论范围内(交给 APM/Crash 做更合理),我理解这些都应该算做精准测试体系的一部分。

关于测试覆盖率的思考


历史上对于测试覆盖率的看法是两极化的,一部分人认为测试覆盖率并不能确保被覆盖到的代码不出问题,所以覆盖率毫无意义;而另一部分人则认为测试覆盖率非常重要,它可以准确反馈出代码的测试充分程度。

Emmmmm...我的看法是既然可以统计到代码覆盖率那么一定可以对应到源码,覆盖度越高/代码执行次数越多不一定代表质量得到了保证;反过来看没有覆盖到的代码则肯定存在风险。任何一种类型的测试都可以理解为基于风险的测试,即有风险就需要测试;100% 无风险的代码其实是无需测试的。基于这个理论,代码覆盖率可以准确找出当前代码中存在风险的部分。

在一篇博文下发现这样一段话,我挺赞同的:

常听到一种说法「代码覆盖率越高不代表代码质量就一定好」,这种说法本身没错,但是如果用这个说法来否定测试分支覆盖的意义,我觉得有点矫枉过正了。这就好比由于功能测试中对需求文档覆盖率越高不一定最终质量好,所以没必要要求对需求文档的覆盖程度是一样的。覆盖率越高,即出问题的风险越低,无论哪种测试,其实最终都是为了降低风险,增强质量信心。

代码覆盖率检测原理剖析


NOTE: 本文覆盖率检测原理针对 LLVM 官方提供方案(事实上这应该是目前同时支持 Objective-C 和 Swift 混编项目最合适的解决方案了)。

LLVM Code Coverage 方案通过使用特定的 flags 开启,开启后 LLVM 前端编译器在编译过程中会将覆盖率数据与源码的映射描述信息插入到 LLVM IR 中并在 link 后写入最终生成的二进制文件。在运行二进制文件时会采集覆盖率数据,最终通过映射信息反映到源码从而生成覆盖率报告。

LLVM 致力于打造普适的覆盖率映射信息,即可以适用于 LLVM 各种前端而不仅仅是 clang(比如上面提到的 swiftc)。此外 LLVM 还会尽可能的缩小覆盖率数据以便于减少 IR 和目标文件的 size,举个 🌰 LLVM 的前端可以通过将函数内的 statements 以 code region 的形式分组的方式取代为每一行 statements 单独映射信息,这样就只需要对分组后的 region 做执行计数啦。

映射区域


NOTE:LLVM 的覆盖率信息基于函数级别的 profile instrumentation counters,对于要求统计代码覆盖率的函数,LLVM 前端需要创建从源码到 profile instrumentation counters 的映射数据。

函数的映射数据包含一组映射区域(Mapping Region,前面聊过为了减少 IR 和目标文件 size 那段提过的 region),每个 region 包含以下信息:

  • file id

  • coverage mapping counter

  • region's kind

region's kind 有以下几种:

1、关联部分源码和覆盖映射计数(coverage mapping counter)的 region,占比最大,被用于代码覆盖率工具计算相应区域的执行次数,高亮未执行的代码区域,也可进一步获得对应函数多样化的代码覆盖率分析。

2、被跳过的 region,表示被预编译阶段跳过的源码部分。因为没有被执行过所以不关联覆盖映射计数,被用于代码覆盖率工具将函数内对应行标记为无需代码计数的代码行。

3、展开区域,表示宏定义展开,带有附加字段 expanded file id,这个附加字段可以帮助代码覆盖率工具找到宏定义对应的源文件进一步定位到对应源码。展开区域自身并不关联覆盖计数,代码覆盖率工具通过其定位到的源码区域内对应 file id 的执行次数来代替这部分区域的计数。

File ID


region 中的 file id 其实就是一个简单的 integer 值,用以标记当前 region 处于哪个源文件或宏定义当中:

计数


映射计数可以理解为对 profile instrumentation counter 的引用,一个 region 的执行次数取决于对应的 profile instrumentation counter。

除了查询对应 profile instrumentation counter 来计算 region 的执行次数外,还有一些其他优化的小细节:

int main(int argc, const char *argv[]) {    // Region's counter is a reference to the profile counter #0
                                           
  if (argc > 1) {                           // Region's counter is a reference to the profile counter #1
    printf("%s\n", argv[1]);
  } else {                                  // Region's counter is an expression (reference to the profile counter #0 - reference to the profile counter #1)
    printf("\n");
  }
  return 0;
}

PS: 这里截图覆盖不到一部分注释所以用代码块来表示(高亮区域看不到了...),简单来说就是用进入 func region 的总次数 - if 判真分支 region 的执行次数 = else 分支 region 的执行次数。

LLVM IR 表示


覆盖率映射数据通过一个全局结构体 __llvm_coverage_mapping 写入 LLVM IR 中(Windows 平台 ".lcovmap$M";其他类 UNIX 系统 "__llvm_covmap"):

{
  { i32, i32, i32, i32 } ; Coverage map header
  {
    i32 0,  ; Always 0. In prior versions, the number of affixed function records
    i32 32, ; The length of the string that contains the encoded translation unit filenames
    i32 0,  ; Always 0. In prior versions, the length of the affixed string that contains the encoded coverage mapping data
    i32 3,  ; Coverage mapping format version
  },
 [32 x i8] c"..." ; Encoded data (dissected later)
}, section "__llvm_covmap", align 8

PS: align 8 是因为 ld64 不能确保从不同的目标文件中紧密的收集 Symbols。

增量代码覆盖率设计方案


技术方案选型

猿辅导 iOS 侧目前新增业务使用 Swift 编写,历史代码因为改动与验证成本较高(风险也比较大)保留 Objective-C 代码不变,目前在精准测试方向业界公开的方案基本都是基于 GCC 实现的并不适用于带有 Swift 源码的工程。

Emmmmm...据我了解,目前业界大厂都正在积极转型 Swift,另外从最近几年的 WWDC 来看 Objective-C 的维护工作也越来越少了,这里还是推荐大家拥抱 Swift 的。

嘛~ Swift 早期有第三方覆盖率工具 SwiftCov 在做覆盖率相关的事儿,不过在 WWDC15 Session 410 小 🍎 放出了自己整合到 LLVM 的代码覆盖率方案后基本就没第三方继续做了。一方面是大家都比较识趣(不存在的),另一方面也说明官方的覆盖率方案的确没明显痛点要补了(主要原因),反而在对 Swift 各个版本的兼容性和 LLVM 集成方面有绝对的优势,所以我们的技术方案选型就省心不少。

WWDC15 Session 410 官方的代码覆盖率检测方案只给出了单测覆盖率统计使用示例,那么如何让打出的包在执行黑盒测试用例时也能检测出代码覆盖率数据呢?

如何生成黑盒测试覆盖率报告?

通过编译 flags 可以开启 LLVM 对于代码测试覆盖率映射信息的生成:

  • clang, -fprofile-instr-generate + -fcoverage-mapping

  • swiftc, -profile-generate + -profile-coverage-mapping

当使用 -fcoverage-mapping 或 -profile-coverage-mapping 时,编译器在编译源码时会生成 profiling instrumentation counters 与 source ranges 的映射描述信息。描述信息会被插入 LLVM IR 中并在 link 后写入最终生成的二进制文件,在 __LLVM_COV Segment 下可以看到若干 __llvm_covmap 打头的 Sections:

另一方面,用 -fprofile-instr-generate 或 -profile-generate 编译源文件时可以生成 instrumented code 并收集上下文敏感的执行计数,最终输出到一个以 .profraw 为后缀的文件中。

Emmmmm...不得不说这个文件的 size 还挺大的,实测会随覆盖路径越来越全面变得越来越大。

这个 .profraw 文件可以用 LLVM 提供的工具 llvm-profdata 转为 .profdata 文件,尺寸会大幅度下降(大概 25MB 转完只剩不到 1MB 多点的样子),不过都不可读。好奇宝宝可以进一步通过工具解出人类可读的格式,画风大概是这样:

/Users/lixin/Documents/yfd/ProjectName/Pods/PodName/Classes/SourceDirName/SourceFileName.swift:$s12SomeServer03AppA3ApiC0cA0C7configsSayAC0cA4ItemCGSgvg
# Func Hash:
0
# Num Counters:
1
# Counter Values:
4

有了 .profdata 文件,还有采集这些数据的二进制内部的映射描述信息,就可以愉快的生成代码覆盖率报告了,具体做法可以参考 LLVM 工具 llvm-cov 的使用介绍(官方方案就是省心,LLVM 全家桶一把梭有木有)。生成的代码覆盖率报告画风大概是这个样子:

PS: 在覆盖率报告最下方还有一行汇总数据,截图没有体现出来。

可以看到每个变动的源文件的代码覆盖情况,从左到右依次是 Filename,Function Coverage,Line Coverage 以及 Region Coverage。点击对应文件的 link 可以跳转到源文件内部查看覆盖细节,这里以 TTLessonListWebViewController.swift 举 🌰:

从左往右依次是 Line,Count 和 Source,可以观察到 Count = 0 的源码是标红高亮显示的。通过用例与代码双向映射精准的测试充分度分析可以帮助我们快速定位到未覆盖的代码,降低上线发布风险,甚至反推出测试用例是否存在遗漏 case 又或代码设计是否存在冗余。

整体流程设计方案


流程描述:

  • 移动客户端工程以 SDK 的形式集成并配置,通过原有 CI Job 打包机打包

  • 将打包机产出的安装包安装到设备上,通过执行测试用例产出代码测试覆盖率数据

  • 设备将产出的覆盖率数据上报服务器,服务器做增量处理并更新覆盖率报告

  • 服务器将覆盖率报告上传至 OSS 云服务,RD/QA 可以通过 OSS link 查看测试覆盖情况

整体设计是无侵入性的,几乎不会带来任何新增工作量的同时就可以在每个版本都收获一份可视化测试用例执行后的代码覆盖率报告她不香吗?

总结与展望


文章开头通过提出问题的方式引出了「精准测试」的概念,进一步聚焦到 iOS 移动端。在交代了目前 iOS 市面上已公开方案大同小异(都是基于 GCC 那套方案),不能满足当前工程需求的前提后介绍了 LLVM Code Coverage 方案,并带领大家一步步分析了官方覆盖率实现原理,希望可以为大家带来价值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值