我们在日常的开发测试中,经常会遇到这样的问题,用户反馈 App 闪退,但却无法复现;完成了一个新功能,常规流程和临界阈值等等都验证通过,但还是觉得差了点什么。之所以有这样的疑惑,是因为我们需要一种自动化测试的手段,来提升 App 的健壮性和稳定性,解放我们的双手,告别点点点。Monkey 工具的出现,解决了我们的痛点。
也许苹果认为自家系统和应用质量比较好,并没有给 iOS 提供 Monkey 工具。但好在有很多热爱技术的人不断突破自我,于是就有了很多 iOS 上的 Monkey 工具。比如基于 UIAutomation 的 Monkey,这也是苹果最早提供的基于自家 Instruments 来进行自动化测试的框架,但UIAutomation存在不少bug且使用js脚本调试很麻烦,很多地方不够完善,苹果在 iOS 9 之后砍掉了 UIAutomation,取而代之的是 XCUITest。
在一段时间里 iOS 没有 Monkey 的说法了,但出了新的XCUITest,总有人思考如何在此基础上展开研究,于是基于 XCUITest 框架的 开源Monkey 工具— SwiftMonkey诞生了,之后国内的 Monkey 也随之兴起,涌现出了一批不错的 Monkey 工具,比如为人熟知的 Fastmonkey。
Monkey 是通过伪随机用户事件来进行自动化压力测试的,它的优缺点都特别明显:
优点:通过随机模拟的用户行为,一般为 Single Tap、Double Tap、Long Press、Move 等最常见的操作,提升应用稳定性,能测出一些不可知的情况还有手动点点点无法测出的问题。
缺点:过于随机,局限性很明显,会导致大量无意义的操作,甚至陷入死循环。
因此,我们需要在这个随机的基础之上,去制定一套更合理、更符合业务流程逻辑的方案。目前可供选择的也就是上面说的 SwiftMonkey 和 FastMonkey,其中FastMonkey是基于SwiftMonkey加上 XCTestWD 结合而成的,功能更强大但也更复杂,而我们只需要一个轻量级的,无需插桩也能运行,同时 XCUITest 自身也包含了足够清晰的日志、截图等等,SwiftMonkey 完全能胜任。
1. 节约人力与时间成本
2. 对APP健壮性有进一步保障
在选择开源Monkey之前,先明确一下我们想要实现的场景:
方便在是否需要插桩间切换:插桩除了能开启Monkey的手势轨迹外,也能更方便调用某些代码如 Scheme 跳转等。
App 常用 Event 检测:比如 App 切换到后台或跳转到其他 App,会导致测试中断无法继续进行。
支持插入功能逻辑:比如登录等业务流程。
Crash 数据收集解析上报:Monkey 测试最终是要能解决问题的,不然就毫无意义。
其实这一 part 比较简单,SwiftMonkey 的标准化接入就是用 CocoaPods 或者手动拖入工程来插桩使用的,那要修改成不插桩的方式,如果了解 XCUITest 的都很清楚,它是支持通过 BundleID 来唤起的,所以只要把 SwiftMonkey 接入到一个新建的 Xcode 工程,然后将 BundleID 修改成任意你想用来进行 Monkey Test 的 App 即可。
let launchApp = XCUIApplication(bundleIdentifier: "com.xxx.xxx")
launchApp.launch()
同样利用 XCUITest 框架已提供的 API,我们能轻松获取到 App 的各类状态,然后基于这些状态去做相应的逻辑处理,例如在每次 actInForeground 执行的时候,一旦检测到用于 Test 的 App 不在前台运行,就先强制唤回,再执行后续的 Test。
if launchApp.state != .runningForeground {
launchApp.activate()
}
这块的实现方式其实有很多种,个人认为相对来说比较优雅、不侵入源码的方式是将需要自定义的 Action 用 closure 的方式往外抛出,自己封装的这一层再做一些通用的 Action,比如登录,然后更偏业务本身的 Action,比如 scheme 唤起具体的 VC,可以继续往外抛,让具体使用的业务模块来决定实现。
func actInForeground(_ action: @escaping ActionClosure) -> ActionClosure {
guard #available(iOS 9.0, *) else {
return action
}
let closure: ActionClosure = {
if launchApp.state != .runningForeground {
launchApp.activate()
}
generalActionClosure() // 调用抛出给外部实现的通用 Action
action()
}
return {
if Thread.isMainThread {
closure()
} else {
DispatchQueue.main.async(execute: closure)
}
}
}
//登录模块 示例
func loginAction() {
if self.currentApp.buttons["短信验证码登录"].exists{
let textField = self.currentApp.textFields.element
if !textField.exists {
return
}
textField.tap()
textField.clearAndEnterText(text: "用户名")
sleep(1)
let pwdField = self.currentApp.secureTextFields.element
pwdField.tap()
pwdField.typeText("******")
sleep(1)
let loginBtn = self.currentApp.buttons["登录"]
loginBtn.tap()
self.isLogin = true
sleep(3)
}
每次执行完 Monkey,可以配置一个 Crash 收集 & 分析的脚本,去查看手机里有没有对应的崩溃日志产生,有的话就提取出来,加上 App 的 dSYM 文件,结合 XCL 提供的符号解析工具将崩溃的堆栈解析出来。
网上(推荐:Google、Stack Overflow、Github )就能找到崩溃日志提取的三方开源工具,比如:
idevicecrashreport -u <device_udid> -e -k ~/Desktop/crashlog
导出日志后解析,比如:
atos -o "myapp.app.dSYM" -arch arm64 -l <base address> <address>
解析出符号之后,就能快速定位到问题代码,从而解决发现的崩溃。
1)目前未做到全自动化,依赖主工程及打包环境,配置表需要手动安装。
2)可以指定模块执行,但还未开展,目前是随机执行。
本文给大家简单分享了 iOS Monkey 测试的基本背景和流程,但这只是万里长征的第一步。有了这些最基础的知识,我们首先必须要搭建一套持续集成的环境,让装包 + Monkey + 崩溃收集完全自动化多设备运行。对于框架本身,我们需要继续打磨,才能让其变得更好用。比如固定在某个模块或者业务范围内执行 Monkey,或者提高 Action 执行的效率等等,这些都是需要花费时间精力去深挖,我们一直致力于更高效便捷地提升测试各个维度的有效性,乐于分享自己在每个阶段的一点点心得与各位一起交流,一起提高。