背景

之前的项目中接触过一些自动化测试相关的内容,但是后来自动化测试的脚本就不是我负责搞了,只是帮忙辅助下,近期的项目以及日常生活(指抢菜啥的)中用到了类似自动化测试相关的方法,所以这篇博客就简单做下面向安卓应用的自动化测试方法总结,并以其中使用无障碍功能完成自动化测试的开源抢菜应用为例看看实现上是否有比较新颖的地方。

目前了解到的测试相关技术

先说下这篇博客想讲的内容主题,首先针对的对象是安卓应用,同时这里测试的含义并不是传统的项目开发中测试功能是否有bug,而是说实现一个类似状态机的系统,模拟用户的操作去完成特定的任务,可能说成脚本比较贴切,但同时我们又希望脚本具有一定的判断能力,而不是按照固定的操作顺序去执行。

目前我了解到的有这三种方法,他们的区别以及优缺点总结如下:

Monkey

Monkey是adb中自带的一种安卓应用测试工具,其名字的由来是猴子的自由点击测试,顾名思义这个工具可以提供类似于自动执行点击相关的一系列操作,通常用在应用测试中。

这款工具在随机测试中应用广泛,可以配置各种不同类型点击的比例,具有可重放测试流程的功能。优点同时也是缺点就是随机测试,一方面随机测试可以针对不同类型的应用,另一方面随机测试对于我们完成特定目标如针对某种功能进行测试,比较难满足。

ADB+图像识别

这种方法将图片作为状态,通过ADB指令实现点击功能,进一步实现状态间的转移。这种目前我接触到的几个项目是跟游戏相关的,一般用来解放双手,完成某些机械化的操作;但是我也见过一些操作精度比较高的脚本,估计点击还是ADB实现的,但是读状态好像是直接读内存(虚拟机环境下),所以时延可以达到毫秒级。此外在获取图像方面也是

这种测试方式的优点是,已经能很好地完成测试以及转移到目标状态的功能,并且开发的成本相对来说比较低,只要有框架;缺点是图像匹配的模式时延可能比较高,并且要去找图像来做匹配,通用性不太好。

无障碍功能

利用安卓的无障碍功能实现的状态读取以及状态转移,使用到的全都是系统提供的接口,毕竟要读取其他应用的信息。比较有名的利用无障碍功能软件如AutoJs,还有各大论文中研究过的密码管理器类的应用。

优点是能够读取的信息比较具体化,包括文字信息,当前的类信息等等,同时操作也能够更加精准化,另外就是能够独立完成测试任务,不用依赖外部主机;缺点是如果测试功能想写得好,开发还是需要一些功夫的,另外图像信息什么的也不太适合在这里进行读取和处理。

抢菜应用学习

有过抢菜经验的朋友都知道,抢菜需要天时(指早起)地利(指网快)人和(指手速),那么抢菜软件可以帮助我们弥补手速的差距。一般来说,使用连点器(也是基于无障碍功能开发)可以满足绝大多数的需要,使用无障碍功能实现的抢菜软件实际上并不比连点器好到哪里去,因为当你要一直要以低速重复某个抢菜流程的时候菜早就被别人抢光了。

反而是连点器这种简单粗暴的东西可以轻松设置每秒近万次的点击,经过我的观察,使用连点器的网络流量比起我手动点击的多了2倍以上。

但是抢菜软件还是有其存在的意义,毕竟如果能考虑到各种情景的话那还是比人力要好用的。所以下面以一个开源的抢菜项目为例看下抢菜是怎么实现的。

首先了解下无障碍功能在安卓中的实现方式。想要提供无障碍功能的应用需要在AndroidManifest中注册一个service服务,需要申请android.permission.BIND_ACCESSIBILITY_SERVICE权限。可以通过xml文件配置无障碍service的参数,包括监听的目标包名,事件等等,下面的官方的一个代码示例:

1
2
3
4
5
6
7
<service android:name=".MyAccessibilityService">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/serviceconfig" />
</service>

这个service必须扩展AccessibilityService类,有四个方法可供重写,包括onAccessibilityEventonInterrupt,前者用于收到系统通知的无障碍功能事件并进行处理,后者用于在无障碍功能暂停时候的反馈(但是有些无障碍功能应用是忽略这个方法的,直接使用空函数)。

那我们通过xml配置文件来看下抢菜应用监听了什么事件:

1
2
3
4
5
<accessibility-service
...
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged"
...
/>

三个事件分别代表窗口状态发生变化,从代码来看主要是判断当前View的类有没有发生改变;第二个事件应该是监听窗口内容发生变化,没有看到有针对这个事件进行特别判断的代码,应该说明这个事件是经常发生的,稍微有操作就会触发这个事件;最后一个应该是监听通知栏事件,在有通知来的时候触发全局返回键,并且只有切换到某个类的时候会进行处理(为啥有些类的类名不包含包名?)。

虽然是kotlin的代码,但还是比较好懂的,其功能的实现也不是严格依赖状态机,我觉得可以总结为根据类名有啥点啥类型,不过还是有一些path sensitive的地方。

主要流程:

  • 接收系统发过来的AccessibilityEvent
  • 获取当前event所在的类名,同时使用标志位判断当前的类是否适合前往结算页面(购物车界面,还有几个选择时间的界面,不太明白为啥选择时间之后适合到购物车页面?);另外还有一个标志位是在主页的时候判断有没有通知,并记录下通知的数量(这个不太理解);
  • 根据当前的类执行对应的操作:
    • 结算页面的类:判断是否包含“立即支付”的文本,并点击;
    • 购物车界面的类:判断是否包含“去结算”的文本,并点击;
    • 选择配送时间的类:识别文本为“-”的界面元素,可能是配送时间,检查是否可以被点击,点击;如果都不能被点击,那么使用返回键;
    • 某个类:不知道是什么类,反正使用返回键;
    • 某个类:执行通知数量的返回键次数;
    • 返回购物车的对话框:判断包含“返回购物车”文本的元素并点击;
    • 默认:点击包含“继续支付”文本的元素,这应该是最后抢下单的时候连点的位置;

综合来看的话,抢菜功能的实现确实没有太多的path sensitive的地方,只有一个购物车到结算页面有这么个判断,但是我感觉也没啥必要,因为用来点击的文本和其他状态并没有冲突。此外还有其他抢菜软件,有些写的应该比这个复杂,但是没有开源,感觉看jeb逆向的源码有点不好看(而且是kotlin写的)。

参考资料

  1. https://developer.android.google.cn/guide/topics/ui/accessibility/service?hl=zh-cn
  2. https://developer.android.google.cn/reference/android/R.styleable?hl=zh-cn#AccessibilityService_accessibilityEventTypes