QT4A通过向被测Android应用进程中注入测试桩,以获取进程中的控件树信息,以及相关的类、对象的属性和方法。
测试桩是使用Java语言开发的jar包(dex),它主要利用Java的反射功能,获取到进程中类和对象的实例。
测试桩中会创建一个Socket服务端,客户端连接后可以获取到控件的ID、坐标、文本、可见性等信息,通过这些信息,用户可以对控件进行查找、获取文本、设置文本、点击、滑动等操作。
除此之外,测试桩还提供了反射获取对象属性、调用函数等能力,这使得QT4A拥有了超越UI测试
的能力。使用者可以利用这些功能来做更多的事情。
注入过程主要利用了droid_inject
和libdexloader.so
这两个文件。droid_inject
主要是使用了ptrace
调试接口,将自己变成被测进程的调试进程,从而拥有了读写寄存器、读写内存等能力。
- 按照当前CPU架构的函数调用约定,构造好参数和堆栈
- 修改
EIP寄存器
(x86)或PC寄存器
(arm),跳转到目标函数地址执行 - 从寄存器获取函数执行结果
- 获取
dlopen
函数在目标进程中的地址 - 在目标进程中调用
dlopen
函数加载SO模块 - 获取SO模块的入口函数地址
- 在目标进程中调用入口函数
- 调用
libandroid_runtime.so
模块中的getJNIEnv
函数获取当前线程的JNIEnv*
指针 - 根据
JNIEnv*
指针获取JavaVM*
指针 - 创建子线程,根据
JavaVM*
指针获取子线程的JNIEnv*
指针(使用子线程可以提升成功率) - 获取
dalvik.system.DexClassLoader
类实例以及构造函数 - 实例化
DexClassLoader
对象,传入要加载的dex
路径 - 获取
dex
中的入口函数,并执行
ptrace
接口只能在以下两种情况下使用:
root
设备上可以注入任意进程- 非
root
设备上只能注入相同uid
的进程
因此,对于非root
设备,需要使用应用的debug
包。由于部分设备的run-as
命令存在bug
,这种情况下需要应用进行重打包。
测试桩运行后会创建一个LocalSocket
服务端,PC端要访问该服务可以使用以下两种方法:
- 使用
adb forward
命令将服务映射到本地的TCP
服务 - 直接使用
adb
协议创建一个透传的socket
通道
第一种方法实现简单,但是存在需要创建本地端口,可能会出现端口冲突问题,影响到性能和稳定性。
第二种方法需要实现adb
协议,但是不需要创建本地端口,性能和稳定性都优于第一种方法。
测试桩使用了JSON
格式数据进行通信,理论上支持任意语言访问。主要包含以下字段:
Cmd
命令字,当前执行的操作名Seq
序号其它字段
都是请求的参数
返回结果会包含Result
字段,里面是命令执行的结果;执行报错时会包含Error
字段。
跨进程(跨设备)通信,需要解决的一个问题,就是如何将两个进程中的对象建立一对一的映射关系。
Java中每个对象都有一个内存地址相关的Hashcode
值,QT4A中使用这个值作为对象的唯一标识,查找控件时返回的也是这个值。之后所有的控件操作都会传入这个值,测试桩会在控件树中根据这个值查找对应的控件实例。
测试桩主要利用了Java的反射机制
,可以在运行时获取到类、对象、属性、方法等实例,从而访问到整个Android
世界。
因此,所有逻辑的入口只能是类
以及静态方法
或静态变量
,这些属于可以直接反射获取的范畴。
总的来说,基本思想就是:从不变到会变
,由已知到未知
。
public final class WindowManagerGlobal {
private static WindowManagerGlobal sDefaultWindowManager;
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
}
WindowManagerGlobal这个类中包含了当前进程中所有控件树的根的列表mRoots
,同时它的实现是个单例,sDefaultWindowManager
中保存了该类的实例。因此,可以使用以下过程获取所有控件树。
+-------------------------------------------+
| WindowManagerGlobal.sDefaultWindowManager |
+------------------+------------------------+
|
|
|
|
v
+-------------+--------------+
| WindowManagerGlobal object |
+-------------+--------------+
|
| +-------+ +-----------+
| +----------> | mView | +--------> | mChildren |
| | +-------+ +-----------+
v |
+----+---+ | +-------+ +-----------+
| mRoots | +----------> | mView | +--------> | mChildren |
+--------+ | +-------+ +-----------+
|
| +-------+ +-----------+
+----------> | mView | +--------> | mChildren |
+-------+ +-----------+
在自动化测试中,除了要对应用进行操作,还需要支持对系统的操作,比如:WIFI、剪切板、截屏、屏幕解锁、权限控制等。这些有部分可以通过shell命令实现,但是大部分的功能都需要额外实现。系统测试桩主要就是用于对Android系统的控制。
系统测试桩也是使用Java语言开发,以独立进程方式运行。主要以下两种执行方式:
- 命令行方式
- 服务进程方式
前者适合低频、返回结果简单的场景,这种方式需要每次都创建新的进程,执行时间会稍长一些;后者适合高频或返回结果复杂的场景,这种方式使用和应用测试桩相同的方式通信,耗时较短。
在非root
手机上,有些操作使用shell
权限是无法完成的(比如操作WIFI),此时,是使用QT4A助手
创建的后台服务进程来操作设备。
Python层主要分为两层,底层是对应用测试桩和系统测试桩接口的封装,上层主要是QT4A对用户的接口。
底层主要包含adb
、androiddriver
、devicedriver
、webdriver
等模块。类关系如下:
+-----+ +------------+
| ADB | | IWebDriver |
+--+--+ +------+-----+
^ ^
| |
| |
+------+-------+ +-------+-------+
| DeviceDriver | |WebkitWebDriver|
+------+-------+ +-------+-------+
^ ^
| |
| |
+------+--------+ +-----+-----+
| AndroidDriver | | WebDriver |
+---------------+ +-----------+
最底层的类是ADB
,它主要提供了ADB相关的操作,以及常见的shell
命令封装,所有对手机的操作最终都会转化为ADB操作。
DeviceDriver是对系统测试桩接口的封装,大部分都是使用了命令行方式,少数使用了服务进程方式。
AndroidDriver是对应用测试桩接口的封装。
WebDriver是针对Android端的qt4w
中的IWebDriver
接口实现,用于支持Android端的Web自动化测试。
上层主要包含device
、androidapp
、andrcontrols
、androidtestbase
等模块。各模块主要功能如下:
device
对设备接口的进一步封装,用户主要使用这里的接口androidapp
应用基类,所有项目都需要创建一个AndroidApp
类的子类作为自己的应用类andrcontrols
窗口基类和Android常用的控件封装,包括TextView
、EditText
、Button
、ImageView
、ScrollView
、ListView
等androidtestbase
测试基类,用户需要创建一个AndroidTestBase
的子类作为自己的测试基类