背景
App中经常会有crash,为了治理crash,我们尝试了各自手段,包括提交代码前的静态检测,单元测试等,但是还是会有crash被带到线上,而一旦上线,所带来的影响和修复成本都是很大的。借鉴我们组Android同事的想法,有目的有策略地对App自动化的探索(类似于智能的Monkey),探索到的页面越多,发现问题的可能性越大。将问题收集提前报出来,就可以避免带到线上。
本章我们对探索的策略不作细说,只聊聊探索在iOS的实现的大概流程。
方案确定
涉及UI测试,第一反应想到的是iOS自带的UI测试。但是iOS的UI测试是黑盒的,只能根据accessibilityIdentifier
等来查找元素操作,能做的有限,显然满足不了探索的要求。而如果在App内实现探索,又处理不了系统的Alert弹窗等场景,所以为了两者兼得,我采用UI测试驱动+App内部探索的方案。
iOS的UI测试运行时会在被测试App之外生成另一个App,来驱动被测试App的行为。我们下面称我们自己的App为Host App,UI测试产生的App为Test App。
下图大致展示整个流程,我会一个个说明。
脚本部分
当我们需要运行探索,任务的第一执行者是我们脚本。
脚本的职责有以下几个:
将探索仓库代码和主仓库集成
为了在Test App和HostApp间通信(Test App触发Host App开始和结束探索),我们需要将探索仓库中Test+Common模块和主工程的Test Target集成,将Host+Common和主Target集成。
其中Common是在Test App和Host App都会运行的公用部分,处理一些公用部分和通信部分。通信部分可以参考我的这篇iOS进程间通信。
运行UI测试
通过xcodebuild
命令编译并运行UI测试。
同时监测:
- 如果UI测试未到时间便退出,搜集本次运行信息,然后重新运行。
- 如果超时退出,则将之前搜集的所有运行信息整理,发出报告。
搜集报告
因为借助了iOS原生的UI测试,所有我们也希望借助系统现有的能力搜集报告。但是发现原生的UI测试有很多crash没有采集到,所以我通过自己在代码中捕获Exception和Signal,然后log出来,最后通过一次UI运行完之后,正则解析日志获取crash信息。
解析日志可以使用xcresulttool
来完成,Test App进程的Host App进程的日志都能获取。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
将UI测试生成的.xcresult目录导出成可分析的log
"""
def export_diagnostics_logs(self, test_result_path, log_dir):
# get logref id
tmp_contents_json = os.path.join(self.results_path, "tmp_contents.json")
self.util.run_command('xcrun xcresulttool get --path {} --format json > {}'.format(test_result_path, tmp_contents_json))
diagnostics_id = None
try:
json_dict = json.load(file(tmp_contents_json))
diagnostics_id = json_dict['actions']['_values'][0]['actionResult']['diagnosticsRef']['id']['_value']
except Exception as error:
LOG.warning("diagnostics id not found")
return False
self.util.run_command('xcrun xcresulttool export --path {} --output-path {} --id {} --type directory'.format(test_result_path, log_dir, diagnostics_id))
return True
Test App 部分
当脚本触发了UI测试,首先会运行的是Test App。
我们在Test App做下面几件事:
- 通过通信机制调用Host App的方法,通知Host App开始探索。
- 定时检测,如果发现Host App中出现了系统弹窗,则点掉弹窗。(这部分Host App做不到)
- 定时检测,如果发现Host App进入了后台,则重新唤醒Host App,因为这时可能点击导致跳出了App。
- 定时检测,如果Host App进入非运行状态,则结束本次UI测试,因为这时候Host App可能已经crash。
总的来说,Test App主要做一个触发和处理弹窗等操作,没有探索的逻辑存在。
Host App
前面做了那么多,其实都是为了探索的流程打通,而到了Host App,核心的职责就是探索页面。
- 设置异常检测,当发生crash时将crash堆栈以一定的格式打印出来,分析报告时会以通用的格式解析。
- 获取页面所有元素,获取可操作的元素,通过策略选择选择一个元素。
- 通过使用UITouch实现模拟点击输入等操作,这部分可以参考KIF和EarlGrey。
- 不断重复2,3,深度遍历App页面,直至探索时间截止。
效果
截止目前为止,一小时探索有80个页面,其中还有很多问题没解决,需要持续完善。
总结
在实现整个流程的过程中,踩了不少坑,同时也对UI测试,进程通信,日志解析等方面加深了理解,踩坑使我进步。
QA
Q:为什么探索仓库和主仓库分开?
A:这是为了和主仓库解耦,探索的所有功能都不会带上线,这样做能保持主仓库的代码的干净,而且探索仓库可以随时迭代,不受主仓库制约。
Q:怎么将探索时间这个参数传入进入App。
A:通过设置工程文件的GCC_PREPROCESSOR_DEFINITIONS
或者在xcodebuild
时传入1
xcodebuild test xxx GCC_PREPROCESSOR_DEFINITIONS="DEBUG=1 EXPLORE_TIME=1200"
然后在iOS代码中,使用以下方式获取,参考Stringification:
1 |
|
Q:Host App异常退出时,为什么不能在Test App多次运行Host App,而要通过脚本重新运行xcodebuild
?
A:因为我们每次运行都需要采集Host App的运行日志,如果通过Test App重启Host App,那么等最后结束,我们只能拿到最后一次Host App运行的日志,会遗漏之前启动运行的页面/崩溃信息。