谈到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上执行。

更新分割线

楼主今天刚好做手术去了,没注意看这个帖子已经被发出来了,搞了个大乌龙。其实这篇文章当时是没写完的。评论里各位提出譬如,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,还是不够稳定和不快,但我相信这些都能够被解决。

最后:下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐