前言

谈到 Android 端的自动化测试框架,大家第一时间想到的是什么?Appium,Uiautomator2,亦或是 Airtest。5 年前,在我最开始接触自动化测试工作的时候,Appium 绝对是做安卓自动化的首选方案,再后来又因为公司变动慢慢接触了 Airtest。

但随着时间的推移,Appium 的缺点也逐渐暴露出来,它运行慢,配置繁琐,学习成本高(对于初学者来说)。不可否认,Appium 是一个非常牛逼的自动化测试框架,它跨平台,跨语言,这一切都是基于它的设计模型采用了经典的 C/S 架构。简单来说,Appium 自己作为一个客户端,然后在待测设备上安装一个服务端。我们的 PC 上测试脚本无论用什么语言编写,最后经过 appuim 的转换生成对应的网络请求,然后再发送给待测设备上的服务端,服务端接受到请求后再调用设备本身的提供的自动化接口,执行完了在将结果数据返回到我们的服务端。以完成一个测试链路。如下图。

所以当我写下一条安卓自动化脚本,它的执行逻辑如下,例如 (伪代码,仅表达个概念):

# 假设是 python 代码 drive.find(text="主页") -> 交给 pc 上的 appium 客户端 转成 js 的 request -> 发送给待测设备上的作为服务端的 apk -> 这个 apk 再去调用系统接口,将执行结果返回给 appium

当一个链路越长,它所要执行的时间也就越长,框架所要做的事也就越多,稳定性相应也是个问题,即使是 Uiautomator2 和 Airtest 也逃不过需要发送和接收请求的过程。他们无非是:

# 假设是 python 代码 drive.find(text="主页") -> 发送给待测设备上的作为服务端 apk -> 这个 apk 再去调用系统接口,将执行结果返回给 Uiautomator2 和 Airtest(Poco),

相比较 appium 只是少了编程语言转换这一过程,而对于这一类测试框架在发送和接收请求的过程中,他们不得不保证一件事,就是服务端需要一直运行在后台,一但后台服务挂了 (被系统杀死,或者 apk 本身出现 crash),测试动作就必然会执行失败。

最后就变成不得不写一些额外的逻辑来做服务端的保活,监听进程,闪退后重启...(在我阅读 Airtest(Poco)的源码时,发现了很多服务端保活的代码逻辑)更极端的是即使做了保活,依然无法正常运行服务端,这容易给使用者在 debug 过程中造成非常大的困扰。同时虽然这些动作不运行在前台,测试人员看不到,但它却实打实的发生。由于不是系统原生的 app,你无法保证他不会对你的系统和你的待测 app 造成影响。

举个实际的例子,我司的产品做的是一个定制版的安卓系统,里面内置了一些应用,他们从开机起是就是常驻前台运行的,同样存在一些保活,断线重连的机制。我们最开始使用 Airtest(Poco,为了方便,后续都直接叫 poco)做自动化,在运行过程中,我们一些测试场景是需要不断重置设备->恢复出厂设置,清空安卓系统内所有非内置的 app。而 poco 在初次安装运行以及服务端 app 闪退恢复的时候都会触发一次后台进程唤起,这种动作会迫使 poco server 强制在前台弹出一次,这使得 poco server 的进程 和我司 app 的进程互相抢占,从而我司 app 被强制退到后台或者直接闪退。测试人员想要提交一个 crash issue 还得先再三分析这是属于 poco 导致的还是应用本身的问题,自动化反而成了一种累赘。

调研

为了解决 这个的问题,我开始思考一件事。做安卓自动化一定要遵循 C/S 架构,通过在手机上安装 server 端才能实现将系统的接口转发出来才能实现对安卓元素的查找和控制吗?

我的直觉告诉我并不是,我之前做桌面端自动化的时候,我也思考了相同的问题。而后来我发现测应用和测试脚本都在 PC 上,我直接就能拿到系统的接口,我为什么还要搞个中间服务去调用系统的 api,这不是多此一举吗。为了干掉 appium 这个中间商。于是我写了https://github.com/letmeNo1/Makima
https://github.com/letmeNo1/Aki 通过直接用 python 和 java 通过用系统的 api,来实现 Windows 端和 Mac 端的自动化框架。有了前面的经验,我开始了类似的调研。

翻阅了几个有名的支持安卓的自动化测试框架的运行原理,我发现他们都指向了一个东西叫做 UiAutomator,摘抄一段来自 uiautomator2 官方文档的介绍:

UiAutomator 是 Google 提供的用来做安卓自动化测试的一个 Java 库,基于 Accessibility 服务。功能很强,可以对第三方 App 进行测试,获取屏幕上任意一个 APP 的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用 Java 语言 2. 测试脚本要打包成 jar 或者 apk 包上传到设备上才能运行。

那有没有办法直接调用 UiAutomator 呢?我又开始了疯狂搜索,最后我发现 adb 命令就提供了 adb shell uiautormator dump,他的作用是保存当前页面的 ui hierarchy 信息成一个 xml 文件并储存在手机中,当我们将其 pull 到电脑上打开时,可以发现,它其实就包含了每个元素节点可以用于定位的属性包括 resource-id,text,class 等,同时也有对应的坐标。

找到这里,我欣喜若狂,因为无论做什么端的自动化,无非都是要实现,查找和控制。有了这份 XML 文件,我就可以在 PC 上完成查找的任务,将其在 python 中转成 xml 对象,通过 xml 对象自带的 xpath(.//*contains(@text =") xpath 表示式查找方式来定位我想要得到的元素,然后便能拿到该元素的坐标,最后直接通过
adb shell input tap x y 来实现对元素的点击以及其他长按拖拽等操作 (比较遗憾的是没法直接 set text,对于一些输入框来说,只能 adb shell input text 来模拟输入)。 这样一来,我们就可以不用再通过在手机上安装服务端 apk 来转发调用安卓系统 api 的请求,将搜索的逻辑放在 PC 上执行。

———————————————2023/7/8 日更新——————————————

楼主今天刚好做手术去了,没注意看这个帖子已经被发出来了,搞了个大乌龙。其实这篇文章当时是没写完的。评论里各位提出譬如,adb uiautomtor dump 慢的问题,其实在我最初想用这个方法搞的时候就发现了,基本调用一次耗时在 5-6 秒甚至 10 秒左右,这明显是不太符合预期的。后来也是各种查资料,翻到了一篇很远古的帖子,https://blog.csdn.net/itfootball/article/details/27958441adb,这篇讲的是 uiautomtor dump 无法抓取动态界面的问题,由此对 uiautomtor dump 底层源码做了分析。而我发现了这个uiAutomation.waitForIdle(1000, 1000 * 10) 这行代码正是影响 dump 速度的一大问题。(无法抓取动态界面的问题暂且按下不表)

waitForIdle 的执行逻辑是等待整个 UI 界面处于 idle 状态,如果说你的页面一直在加载动画或者渲染 UI,则无法进入到 Idle 界面。而1000 * 10 表示最大超时时间为 10s,等待超过 10s,无论进没进 idle 状态都强制 dump 一次,如果还没进入 idle 则报错,无法 dump。当时暂时没考虑抓取动态界面的问题,想着先把抓取速度的问题解决了,所以开始了漫漫反编译之路。

我最开始想着说既然你 waitForIdle 会很慢,那我直接从底层源码里把你干掉不就好了。于是乎,我把 uiautomator.jar 从 system/framework 中拽了出来,强行反编译它,去掉了 waitForIdle 了。结果这样做速度是快了很多,但是另一个问题随之而来,这样一旦遇上有 app 动态页面的地方 uiautomator.jar 就会先报错,再调用就直接不能用了。直到你重启当前的 app,它才能恢复正常。这显然也是不符合预期的。

于是乎,我又做了一次魔改。添加一个参数选择,让 waitForIdle 变成一个可传参的格式。反编译主要用到了 dex-tools-2.2,recaf-2.21 这俩工具,直接改 JVM 字节码达到不重新编译,修改源码的目的,大家如果有兴趣,我可以再单开一个帖子写这个。修改后的源码如下,这样就实现了在调用 uiautomator dump 的时候加上--waitForIdle 的来指定最大超时时间。

做到这一步,速度基本上能控制在一个稍微能接受的范畴,每次 dump 大概 1-2s 左右,如果页面没有发生变动,则不触发重新 dump 这样,当我不执行操作,只是单纯检查页面上的 UI 的时候,就可以不用重复发起 dump。

但这样仍存在一个很致命的问题,就是前文提到的无法抓取动态界面。正因如此,我即使想在系统 app 秒表运行过程中,想要 dump 他的 UI 元素都做不到。带着这个问题,我最终还是投入的了 uiautomator2.0 的怀抱。由于 uiautomator2.0 是以 apk 的形式存在,而不是 jar 包,所以对于一个之前没搞过安卓 apk 的小白来说,又是一轮新的 search。包括说如何让 uiautomator2.0 不需要挂载在系统后台,也能实现对 dumpUI 操作。

一顿搜索后,锁定了这篇文章https://blog.csdn.net/cxq234843654/article/details/52605441,巧的是,这还是前公司的一位前辈写的,不得不说人外有人天外有天。

基于前辈的思路,我搞了个空白 app,并添加了测试用例,测试用例内容就是简单的 dump 一下当前 UI。然后将空白 app.apk 和其对应的测试用例 apk 安装到手机上。通过执行 adb shell am instrument -w -r -e class com.hank.dump_hierarchy.HierarchyTest com.hank.dump_hierarchy.test/androidx.test.runner.AndroidJUnitRunner 来实现用 uiautomator2.0 dump UI 的操作,这一过程不需要启动 apk,全程静默执行。

速度相对还行。。但有时候也要 1-2s 左右,PC 上对 xml 查找的时间几乎可以忽略不计,大头还是在 dump 方面。一方面跟页面元素的多少有关系,一方面跟也跟 adb 通信的机制关心,一方面也可能跟 uiautomator2.0 测试用例的执行有关系,我会持续调研,持续更新。

解决方案

这是最终成品代码的地址。后续会慢慢更新 readme,以及不断完善这个项目。
https://github.com/letmeNo1/Nico

结尾

关于文中提到的几个自动化测试框架的底层原理,这边只是简单描述,因为相信已经有很多资深的前辈写过很多分析帖子,大家想要了解,很容易就能搜到,我就不在这班门弄斧了。前言里的废话可能有点多,但我想给大家表达的一个理念是,前人做得东西固然好,但也不是尽善尽美的,我们要学会质疑,多问为什么要这么做,为什么不这么做

当然这个框架目前还是有一定的缺陷,例如输入,目前来说输入这块还是直接调用 adb shell input text 来做的,相较于 Airtest 使用自己安装输入法和直接给元素对象 set text,还是不够稳定和不快,但我相信这些都能够被解决。


↙↙↙阅读原文可查看相关链接,并与作者交流