UiAutomator Nico,一个基于纯 adb 命令实现的安卓自动化测试框架

hank.huang · 2023年07月14日 · 最后由 回复于 2024年11月13日 · 17174 次阅读

前言

谈到 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,还是不够稳定和不快,但我相信这些都能够被解决。

共收到 19 条回复 时间 点赞

挺好的,支持一波

Nico,一个充满猎奇神秘以及欲望的某个字母 APP🐿

看着很好,mark 一下,回头研究一下,少了一个 github 链接(虽然也可以找到)
toast 以及那种时间轮之类的不知是否有例子

移动端框架搞中间层是为了屏蔽不同平台之间的差异,让 client 端使用同一套协议实现不同的平台自动化,相当于降低 client 端的复杂度,固定的 DSL 可以让项目会更好维护。中间抽象层的损耗个人感觉不大,就是 pc 端发请求到移动端的 server,server 调用平台接口实现。这里逃不掉一次通信,除了通信外其他的本地工作只要没 bug 都不会带来损耗。

楼主的想法是通过 adb 命令去 dump 控件,有没有想过 adb 命令一样会出现通信拥挤导致不可接受的卡顿延迟问题?因为这里本质上还是需要手机和外部通信,只是说 appium 走网络协议(http),adb 可以是数据线也可以是网络协议。

思来想去,好像本质问题依然还在?

在 pc 端实现查找任务有个问题,就是延迟比较大。
每 dump 一次都会耗费一点时间。连续 dump 查找元素,就会明显感觉慢。用 accessbility dump 是最快实现速度是毫秒级的的,需要与 pc 端保持实时通信,但是 dump 的效果不好,缺少元素信息。
adb 命令 dump 最慢,基本不用考虑。
用 uiautomator2 dump 也会比较慢,但是元素收集相对比较全,我认为是目前安卓端 dump 布局的最佳方案。

😂 哈哈哈这篇文章其实没写完,还在草稿箱中,一不小心给发出来了

bioboy 回复

@bioboy @ 王稀饭 抱歉,这篇文章其实没写完,就给出来了,关于二位提出的疑问,我稍后更新解答

写的挺好,也都去实践了,赞。但最终还是回到了 U2,既然是以 apk 的形式了,那其实就不如做成服务的形式去请求,这样对于很多操作和中文输入等都友好,不是吗。我提供个思路吧,通过反射连接 UiAutomation 的方式去实现,用 app_process 方式去运行,只需要 push 到手机中,无需安装。本人以前实现过没问题,但因为不是安装的方式,所以一些功能欠缺,比如 wifi 连接等,就搁置了😂

王稀饭 回复

是的,但其实像 window mac,这种执行方和被执行方都在同一端的其实就非常没法必要再搞个乱孤单通信,appium 这种感觉有点多此一举。

hank.huang 回复

这么说确实是,不过考虑到 appium 本身的初衷,它留着一层中间层也无可厚非,因为完全没必要因为 windows 和 mac 而去掉它再维护一套;毕竟你也不能排除它用这台 windows,通过远程通信去 dump 另一台 windows 的控件嘛。

不过文章确实精彩

hank.huang · #11 · 2023年07月20日 Author
仅楼主可见

其实很多看似的没必要,背后都是为了兼容和处理不同场景。毕竟 appium 是一个同时兼容 Android 和 iOS 两个平台,底层还要集成不同的驱动类型。如果你这一套能实现到 appium 类似的体量,效果和程度,最后的效率提升会有多少呢?

从大部分的用户使用来看,这种方式起码保证了我只要专注于怎么使用和 selenium 一脉相承的语法去维护我的用例,甚至于同一套用例,只需要区分不同平台的 locator 就可以做到公用,牺牲一点执行时间是可以接受的。而且执行时间长,其实可以通过并发执行等方式去提升,一定程度上是可以接受的。

Jerry li 回复

提升效率是一方面,另一方面是希望能减少链路调用,以不同的思路去解决自动化测试的问题,永远没有一种框架是最好的,只能说是最合适的。我做这个框架的初衷也只是为了解决我们公司的 APP 的问题啦。只是在调研的过程中延申出了这些思考,如果能做到 appium 那样的通用,且更轻便不是刚好吗😂 当然这是一个美好的愿景,我也在朝这方面努力着

hank.huang 回复

嗯嗯,加油!

刚好最近需要用到 airtest,参考一波,支持一下

mark 一下 后续学习

其实可以不用安装 apk,用 u1 的 jar 包就可以,我现在遇到的问题是 pip 安装后,用 pyinstaller 打包成 exe 时,资源 jar 包获取不到,请问怎么解决,谢谢

hank.huang [该话题已被删除] 中提及了此贴 07月06日 14:12

牛逼牛逼牛逼

react native 打包的 app 因为源代码缺少(testId)appium 没是没办法定位的,元素完全找不到。。。dump 元素自己查的方式确实是可以使用的

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