iOS 测试 iOS (Object-C) 非单元测试状态下代码覆盖率获取尝鲜

陈恒捷 · 2016年12月10日 · 最后由 一无所知的小白 回复于 2018年11月12日 · 5864 次阅读
本帖已被设为精华帖!

最近开始要调研精准测试,代码覆盖率是其中一个十分重要的部分,因此参考网上的流程来试做一下。过程中发现由于 Xcode 8 有一些变化,网上的相关文章都没有对应更新,所以发文记录下。

目标

在操作应用过程中,通过一个特定操作可获取到从应用启动到此操作前所有操作的覆盖率数据。

需要用到的工具

  • XcodeCoverage
  • Xcode 8.1(为了支持 iOS 10.1,亲测可在最新的 iOS 10.1.1 中成功获得覆盖率)

配置步骤

项目加入 XcodeCoverage

只用过 pod 的方式,也建议使用这个方式安装,比较快捷。

  1. 在 Podfile 中加入 pod 'XcodeCoverage', '~>1.0'
  2. 运行 pod install
  3. 在需要获取覆盖率的 target 中加入 Run Script 的步骤,内容为 Pods/XcodeCoverage/exportenv.sh
  • 官方最新的 1.3 貌似不兼容 Xcode 8 ,需要做一些调整。
  1. 下载最新的官方代码:git clone https://github.com/jonreid/XcodeCoverage.git
  2. 拷贝官方的 lcov-1.12/bin 内所有文件到项目所在的 Pod/XcodeCoverage/lcov-1.12/bin 中,同名文件直接覆盖

项目配置添加获取代码覆盖率的相关配置

一般情况下,我们只在 Debug 包中获取代码覆盖率,因此只改动 Debug 的配置。需要改动的地方有两个,以下直接贴图:

项目源码中添加生成覆盖率的相关代码

AppDelegate.m 中添加如下代码 (代码参考自 vigossjjj/CodeCoverage4iOS 项目):

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    ...
        #if !TARGET_IPHONE_SIMULATOR
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirectory = [paths objectAtIndex:0];
            setenv("GCOV_PREFIX", [documentsDirectory cStringUsingEncoding:NSUTF8StringEncoding], 1);
            setenv("GCOV_PREFIX_STRIP", "13", 1);
        #endif

        extern void __gcov_flush(void);
        __gcov_flush();
    ...
}

这部分代码触发点是点击 home 键应用进入后台时。其中 #if !TARGET_IPHONE_SIMULATOR 中包含的部分会改写环境变量,让生成的覆盖率文件( .gcda 文件)放在应用的沙箱中。否则默认会保存在对应 mac 电脑的路径,但由于应用没有写入 mac 电脑路径的权限,因而一直无法生成。

获取覆盖率

获取模拟器运行覆盖率

  1. 在 xcode 中 build ,然后 run
  2. 在应用中做出操作
  3. 点击 home 键,应用进入后台,同时应用自动生成对应覆盖率文件
  4. 进入 项目目录/Pod/XcodeCoverage,运行命令 ./getcov --show ,即可自动生成覆盖率报告。

获取真机运行覆盖率

  1. 在 xcode 中 build ,然后 run
  2. 在应用中做出操作
  3. 点击 home 键,应用进入后台,同时应用自动生成对应覆盖率文件
  4. 打开 Xcode->window->Devices ,选择运行应用的真机
  5. 在 Installed Apps 部分选择运行的应用,然后点击底部的齿轮按钮,选择 'Download Container'
  6. 把生成的 xcappdata 文件保存下来,然后在 finder 中打开
  7. 对 xcappdata 文件右键->显示包内容(show package content),然后打开 AppData/Documents/arm64/,拷贝里面的所有 .gcda 文件
  8. 进入 项目目录/Pod/XcodeCoverage,打开 env.sh,找到 OBJECT_FILE_DIR_normalCURRENT_ARCH
  9. 打开应用编辑结果存储的目录,对应的路径为 <OBJECT_FILE_DIR_normal>/<CURRENT_ARCH>,其中 OBJECT_FILE_DIR_normalCURRENT_ARCH 是上一步找到的值。(大于小于号不需要保留)
  10. 把第 7 步的 .gcda 文件全部拷贝到第 9 步打开的目录
  11. 回到 项目目录/Pod/XcodeCoverage ,运行命令 ./getcov --show ,即可自动生成覆盖率报告。

报告样式:

主要的坑就在真机运行和模拟器运行时目录的区别。 真机运行时,真机上应用无法写入 mac 上的路径,因此 .gcda 文件只能放在手机沙箱,另外取出。模拟器无此问题(模拟器有写入 mac 路径的权限)

后续

覆盖率拿到了,但过程仍然不够自动化,还需要解决覆盖率数据自动回传、通过按钮触发 dump 等问题才能达到最基本的要求。后续再往这个方面继续探究。

参考文档

https://github.com/jonreid/XcodeCoverage/blob/master/README.md

https://github.com/jonreid/XcodeCoverage/issues/48

https://github.com/vigossjjj/CodeCoverage4iOS/blob/master/README.md

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
最佳回复

@chenhengjie123 @season_fly
请问一下,按照步骤一步一步来的,但是在 build run 时,__grov_flush() 出现 link 错误,请问是否知道什么原因
使用模拟器,Xcode 版本 9.4.1,XcodeCoverage 1.3.1

Showing All Issues
  "__gcov_flush()", referenced from:
      -[AppDelegate applicationDidEnterBackground:] in AppDelegate.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

多谢答疑

@Snail5354 @chenhengjie123 已经解决了。恒捷的示例里,是在 AppDelegate.m 导入运行__gcov_flush(),但是我们的项目是混编的,是 AppDelegate.mm,是按照 C++ 来编译的, 开始没有注意到这个差距。
在方法外部导入后可以编译成功。

……
extern "C"{
    void __gcov_flush(void);
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
……
    #if !TARGET_IPHONE_SIMULATOR
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    setenv("GCOV_PREFIX", [documentsDirectory cStringUsingEncoding:NSUTF8StringEncoding], 1);
    setenv("GCOV_PREFIX_STRIP", "13", 1);
    #endif
__gcov_flush();
}
共收到 37 条回复 时间 点赞

过程不够自动化

#1 楼 @Lihuazhang 所以我只是写尝鲜,后面做成自动化还有不少东西要做。

思寒_seveniruby 将本帖设为了精华贴 12月11日 20:28

其实需要做一个 sdk, 把覆盖率的数据直接发送到其他的服务器. apple 的 gcov 方案少了一些关键的功能, 不是一个完备的 gcov.
xcode 把覆盖率的收集和展示做到一块了. 但是没提供自动化的工具. 才导致覆盖率统计很麻烦. 现在的方案不越狱的确难自动化的.

#4 楼 @seveniruby 是的,长期需要往 sdk 方向走。但必须越狱的话难度有点高,毕竟越狱设备相对来说都比较有限。

后面先具体调研下 gcov 的覆盖率生成相关功能,看是否满足需要。


我按着文档配置好之后,启动 app,然后 control+shift+h 将 app 切到后台,然后去生产文档,一直失败,请问是什么原因呢 @chenhengjie123

#6 楼 @aya 看下你的那个 Users/jiasun..../Objects-normal/x86_64 目录有没有 .gcda 文件,也看下 xcode 里面的日志有没有报错?

这个东西 做出报告只是第一步 后面的才是更重要的

试了下发现收集到的覆盖率数据和 xcode 里面看到差别很大,这个会是什么原因

Mo 回复

能详细说一下不?

这个功能刚好和我的实现方式一致:
第一步:我采用 linux 作为后台,部署了 xcodecoverage,同时会将编译主机的原文件代码 gcno 拷贝过来,这个可以在 jenkis 里面写 shell 实现;
第二步:用嵌码的方式来收集和发送每个版本每台手机的覆盖率信息,既然用到了 pod ,何不继续加几行代码: 在 pod 里面实现一个接口,用于gcovflush(), 和找到覆盖率文件发送;同时由于是 debug ,我们的开发在调试面板上实现了接口,所以你可以在调试面板上加开关实现是否需要采集和发送覆盖率的开关;做好不要在4g下发送的防护,否则,大家知道的.....。
第三步 linux 后台用 python flask(你也可以添加 java http 接口)实现一个简单的 web 服务器,接受文件,和展示 index.html 这个覆盖率信息;

这样你通过 xcodecoverage 就能自动统计每台手机当天完成的覆盖率,同时也能合并产生当前版本的总的覆盖率;

其中第二步最坑,由于文件太多,如果用 http zip 传输,会崩溃;放弃
如果实现 ftp 客户段传输,发现传不进文件,可能是使用 iphone 的原因;
所以只能启用后台线程一个一个的发送,你可以尝试 gcd 异步机制 ,启动多个线程共同完成工作;
当然 gcovflush() 只能在主线程刷新,刷新期间会较慢,此时会阻塞 ui ,不过这点时间对于测试人员来说还是值得的;
过段时间会在 github 上上传这些代码,不过最近还在解决从前端 web 自动配置增量信息,而手机只发送那些增量相关的覆盖率信息到 linux 后台收集和展现的过程,如果能解决第二步就能完美解决,因为采用 ftp 传输,可以先在 linux 上以文件的形式保存传输过来的增量模块,手机 pod 接口主动下载该文件做参考传输,这样你就能和 git diff 这些版本比对工具完美结合,成为一个覆盖率自动化工程了,希望能帮到大家;

陈恒捷 回复

用真机跑单元测试,按照上述方法获得的统计结果只有 2.x%,我对比了一下 xcode 运行以后的结果,发现同一个文件在 xcode 里面显示已经覆盖,但是在结果中是未覆盖状态,是否是在哪个细节上配置出了问题

xww 回复

赞!
由于项目原因,最近主要在做 java 的覆盖率,iOS 没进一步研究。期待你发文详细分享下你的实践~

Mo 回复

能给下图片对比下具体哪里不同不?

xww 回复

我也在看你说的实现方式,我想请教 pod 里面实现的接口,是上传 gcda 文件么?还是什么?你在 linux 部署 xcodecoverage 是为了执行./getcov --show 吗?

xww 回复

我在 linux 上部署了 XcodeCoverage 为什么不能执行呢?一直报错Out of memory!
Reading tracefile Coverage.info
lcov: ERROR: no valid records found in tracefile Coverage.info
Reading tracefile Coverage.info
lcov: ERROR: no valid records found in tracefile Coverage.info
cat: /.xcodecoverageignore: No such file or directory
Reading tracefile Coverage.info
lcov: ERROR: no valid records found in tracefile Coverage.info
Reading data file Coverage.info
genhtml: ERROR: no valid records found in tracefile Coverage.info

陈恒捷 回复

season_fly 一直没有回复我,我请教你一下,部署到 linux 上,也把打包过程中的 gcno 文件复制过去了,可是执行报错,你有思路么?

不二家 回复

从日志里只能看出 Coverage.info 这个文件有问题,没有有效的记录。因为不知道这个文件是怎么来的,内容是啥,也无法追查下去。

建议把你详细的部署步骤、操作步骤(包括你觉得和报错有关的所有细节)和出错日志都发出来?

不好意思 各位,最近忙其他东西,也没时间来回复一些问题, 我统一答复一下把:
首先纠正我上面的回帖的两个问题:

  1. 部署在 linux 上
  2. gcov 阻塞 ui 这段时间其实还在完善这个工程: 1.服务器是用来接收手机 gcda 文件的,这个暂时只能部署到 mac 机器,而且是装了 xcode 的 ,因为发文章的时候,我其实还是思路阶段,没有实践到那步,但现在看来是需要 xcode 才能打开 ios 的.h 和.m 文件,很大一个坑,不过迁移到 mac 机器上也很快; 2.gcov 阻塞问题被我使用了 gcd 的信号量机制解决,现在对于测试人员来说,基本没有感觉,但我们工程3000多个源文件需要统计,所以如果使用 zip 压缩,理论上更好,但超过了 afnetworking 这个网络库的最大文件限制;所以还是决定用一个一 个的发送方式 3.期间考虑使用并发来发送,结果 cpu 直接飚升到200%,太费经了,最后就单线程后台异步的方式处理,3000多个文件20s 内也能发完 ,对 ui 没影响,我觉得现在是比较满意的方式 接下来阐述一下流程:
  3. ios 增加一个 pods,这个 pods 就两个文件,一个.h 和一个.m 。文件的内容就是找到 document 下的 gcda 文件,然后调用 gcov_flush 把缓存 gcda 刷新到磁盘,第三步,使用 gcd 的方式发送这些文件到你的 web 服务器
  4. web 服务器很简单,接受这些文件,然后根据传过来的参数:版本号,udid 等,在服务器端创建目录和保存文件 3》后台写一个命令行工具(我暂时是用 shell 做的),找到相应的源代码和 gcno 文件,和 gcda 聚合在一个目录,然后用 lcov 来生成报告就可; 由于现在我们打包是用的开发的 jenkis 服务器机器,它不准我们在上面部署我们的 web 服务器和 gcov 和 lcov,所以我使用了 ssh 主动推送了它上面的编译器信息(.ipa,gcno,源代码)到我的 mac 机器上,我遇到了一个比较难的问题,就是这样产生的 gcno 文件,它里面的源代码路径会指定为 jenkis 上的目录,你需要在本地创建一个一抹一样的目录,来保存源代码信息;这样把上面的流程就完全自动化串了起来, 我本以为可以交差了,但我们领导还想做行的差异性覆盖率信息,我这几天忙着搞 git diff 的东西 怎么和 lcov 结合,被整的死去活来,但如果解决了上述几个步骤 ,基本上也能达到一键自动化生成覆盖率信息的目标了;
不二家 iOS 手工测试代码覆盖率获取 中提及了此贴 05月02日 14:50

尴尬了

undef: __Z12__gcov_flushv
Undefined symbols for architecture x86_64:
  "__gcov_flush()", referenced from:
      -[TestAppDelegate applicationDidEnterBackground:] in TestAppDelegate.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Showing first 200 warnings only

我真机没有这个文件夹 AppData/Documents/arm64/,知道为什么吗?

那有可能是你的覆盖率没有收集到。你把详细步骤和日志发下?

陈恒捷 回复

1

楼主大大,按照你的方式做了,但是 AppData/Documents/下就是没 arm64 这个文件夹

@chenhengjie123 @season_fly
请问一下,按照步骤一步一步来的,但是在 build run 时,__grov_flush() 出现 link 错误,请问是否知道什么原因
使用模拟器,Xcode 版本 9.4.1,XcodeCoverage 1.3.1

Showing All Issues
  "__gcov_flush()", referenced from:
      -[AppDelegate applicationDidEnterBackground:] in AppDelegate.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

多谢答疑

剪烛 回复

symbol(s) not found for architecture x86_64

看起来是模拟器(x86_64)不支持。你用真机试试?

陈恒捷 回复

使用真机 iPhone6p 11.3.1 Xcode 版本 9.4.1,XcodeCoverage 1.3.1 也是同样的错误。
(不使用这段代码,是可以正常编译运行的)
能否问一个小白一点的问题,看这个代码,是运行 C 的方法__gcov_flush() 是么?那这个方法是位于 XcodeCoverage 的哪个文件里边呢?从 XcodeCoverage 的项目里边看没有看见任何相关的代码,是否是另外的项目 CodeCoverage4iOS 提供的方法呢?

剪烛 回复

这个方法是 gcc 编译器提供的,不是 XcodeCoverage 的。

真机使用 arm ,理论上不应该会有 x86_64 的错误,能不能把详细日志发上来看看?

30楼 已删除

找到一篇文章,里面有提到类似的情况,你参考看下?
https://www.jianshu.com/p/3a62abf85984

我也遇到跟你一样的问题 请问你解决了吗
Undefined symbols for architecture arm64:
"__gcov_flush()", referenced from:
-[AppDelegate applicationDidEnterBackground:] in AppDelegate.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

@Snail5354 @chenhengjie123 那个帖子仍然没有解决我的问题。但是另外新建一个 demo,却是可以跑通的。目前正在找开发人员协助

@Snail5354 @chenhengjie123 已经解决了。恒捷的示例里,是在 AppDelegate.m 导入运行__gcov_flush(),但是我们的项目是混编的,是 AppDelegate.mm,是按照 C++ 来编译的, 开始没有注意到这个差距。
在方法外部导入后可以编译成功。

……
extern "C"{
    void __gcov_flush(void);
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
……
    #if !TARGET_IPHONE_SIMULATOR
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    setenv("GCOV_PREFIX", [documentsDirectory cStringUsingEncoding:NSUTF8StringEncoding], 1);
    setenv("GCOV_PREFIX_STRIP", "13", 1);
    #endif
__gcov_flush();
}
剪烛 回复

赞!可以单独开篇帖子说下这个问题了

陈恒捷 回复

Q_Q 感觉不是太需要,遇到类似问题通过搜索应该自然能找到这里来。

@heyzhuliye hi,大大!之前你提到生成报告只是第一步,后面还需要做一些什么事呢,能方便介绍一下不? thx!

剪烛 回复

赞赞赞,很厉害,我跟你是一样的情况,按照你的说明也没有报错啦
不过,我现在没有生成 gcda 文件,还在检查

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