UIAutomator2 简明教程

简介

uiautomator2 是一个自动化测试开源工具,仅支持 android 平台的自动化测试,其封装了谷歌自带的 uiautomator2 测试框架,可以运行在支持 Python 的任一系统上,目前版本为 2.10.2

开源库地址:https://github.com/openatx/uiautomator2

工作原理

如图所示,python-uiautomator2 主要分为两个部分,python 客户端,移动设备

  • python 端:运行脚本,并向移动设备发送 HTTP 请求
  • 移动设备:移动设备上运行了封装了 uiautomator2 的 HTTP 服务,解析收到的请求,并转化成 uiautomator2 的代码。

整个过程

  1. 在移动设备上安装 atx-agent (守护进程), 随后 atx-agent 启动 uiautomator2 服务 (默认 7912 端口) 进行监听
  2. 在 PC 上编写测试脚本并执行(相当于发送 HTTP 请求到移动设备的 server 端)
  3. 移动设备通过 WIFI 或 USB 接收到 PC 上发来的 HTTP 请求,执行制定的操作

安装与启动

安装 uiautomator2

使用 pip 安装

pip install -U uiautomator2

安装完成后,使用如下 python 代码查看环境是事配置成功

说明:后文中所有代码都需要导入 uiautomator2 库,为了简化我使用 u2 代替,d 代表 driver

import uiautomator2 as u2

# 连接并启动
d = u2.connect()
print(d.info)

能正确打印出设备的信息则表示安装成功

注意:需要安装 adb 工具,并配置到系统环境变量,才能操作手机

安装有问题可以到 https://github.com/openatx/uiautomator2/wiki/Common-issues 这里查看一下有没有相同的问题

安装 weditor

weditor 是一款基于浏览器的 UI 查看器,用来帮助我们查看 UI 元素定位。

因为 uiautomator 是独占资源,所以当 atx 运行的时候 uiautomatorviewer 是不能用的,为了减少 atx 频繁的启停,就需要用到此工具

使用 pip 安装

pip install -U weditor

查看安装是否成功

weditor --help
出现如下信息表示安装成功

运行 weditor

python -m weditor
#或者直接在命令行运行
weditor

连接 ADB 设备

可以通过 USB 或 Wifi 与 ADB 设备进行连接,进而调用 Uiautomator2 框架,支持同时连接单个或多个 ADB 设备。

USB 连接

只有一个设备也可以省略参数,多个设备则需要序列号来区分

import uiautomator2 as u2

d = u2.connect("--serial-here--")

# 一个设备时,可简写
d = u2.connect()

无线连接

通过设备的 IP 连接 (需要在同一局域网且设备上的 atx-agent 已经安装并启动)

d = u2.connect("10.1.2.3")

通过 ABD wifi 等同于下面的代码

d = u2.connect_adb_wifi("10.0.0.1:5555")
#等同于
+ Shell: adb connect 10.0.0.1:5555
+ Python: u2.connect_usb("10.0.0.1:5555")

Driver 管理

获取 driver 信息

d.info
# 输出如下
{
"currentPackageName": "com.android.systemui",
"displayHeight": 2097,
"displayRotation": 0,
"displaySizeDpX": 360,
"displaySizeDpY": 780,
"displayWidth": 1080,
"productName": "freedom_turbo_XL",
"screenOn": true,
"sdkInt": 29,
"naturalOrientation": true
}

获取设备信息

会输出测试设备的所有信息,包括电池,CPU,内存等

d.device_info
# 输出如下
{
"udid": "61c90e6a-ba:1b:ba:46:91:0e-freedom_turbo_XL",
"version": "10",
"serial": "61c90e6a",
"brand": "Schok",
"model": "freedom turbo XL",
"hwaddr": "ba:1b:ba:46:91:0e",
"port": 7912,
"sdk": 29,
"agentVersion": "0.9.4",
"display": {
"width": 1080,
"height": 2340
},
"battery": {
"acPowered": false,
"usbPowered": true,
"wirelessPowered": false,
"status": 2,
"health": 2,
"present": true,
"level": 98,
"scale": 100,
"voltage": 4400,
"temperature": 292,
"technology": "Li-ion"
},
"memory": {
"total": 5795832,
"around": "6 GB"
},
"cpu": {
"cores": 8,
"hardware": "Qualcomm Technologies, Inc SDM665"
},
"arch": "",
"owner": null,
"presenceChangedAt": "0001-01-01T00:00:00Z",
"usingBeganAt": "0001-01-01T00:00:00Z",
"product": null,
"provider": null
}

获取屏幕分辨率

# 返回(宽,高)元组
d.window_size()
# 例 分辨率为1080*1920
# 手机竖屏状态返回 (1080,1920)
# 横屏状态返回 (1920,1080)

获取 IP 地址

# 返回ip地址字符串,如果没有则返回None
d.wlan_ip

Driver 全局设置

settings

查看 settings 默认设置

d.settings
#输出

{
#点击后的延迟,(0,3)表示元素点击前等待0秒,点击后等待3S再执行后续操作
'operation_delay': (0, 3),
# opretion_delay生效的方法,默认为click和swipe
# 可以增加press,send_keys,long_click等方式
'operation_delay_methods': ['click', 'swipe'],
# 默认等待时间,相当于appium的隐式等待
'wait_timeout': 20.0,
# xpath日志
'xpath_debug': False
}

修改默认设置,只需要修改 settings 字典即可

#修改延迟为操作前延迟2S 操作后延迟4.5S
d.settings['operation_delay'] = (2,4.5)
#修改延迟生效方法
d.settings['operation_delay_methods'] = {'click','press','send_keys'}
# 修改默认等待
d.settings['wait_timeout'] = 10

使用方法或者属性设置

http 默认请求超时时间

# 默认值60s,
d.HTTP_TIMEOUT = 60

当设备掉线时,等待设备在线时长

# 仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
d.WAIT_FOR_DEVICE_TIMEOUT = 70

元素查找默认等待时间

# 打不到元素时,等待10后再报异常
d.implicitly_wait(10.0)

打开 HTTP debug 信息

d.debug = True
d.info
#输出
15:52:04.736 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "0eed6e063989e5844feba578399e6ff8", "method": "deviceInfo", "params": {}}' 'http://localhost:51046/jsonrpc/0'
15:52:04.816 Response (79 ms) >>>
{"jsonrpc":"2.0","id":"0eed6e063989e5844feba578399e6ff8","result":{"currentPackageName":"com.android.systemui","displayHeight":2097,"displayRotation":0,"displaySizeDpX":360,"displaySizeDpY":780,"displayWidth":1080,"productName":"freedom_turbo_XL","screenOn":true,"sdkInt":29,"naturalOrientation":true}}
<<< END

休眠

# 相当于 time.sleep(10)
d.sleep(10)

应用管理

获取当前界面的 APP 信息

d.app_current()
# 返回当前界面的包名,activity及pid
# {
# "package": "com.xueqiu.android",
# "activity": ".common.MainActivity",
# "pid": 23007
# }

启动应用

# 默认的这种方法是先通过atx-agent解析apk包的mainActivity,然后调用`am start -n $package/$activity`启动
d.app_start("com.example.app")

# 通过指定main activity的方式启动应用,等价于调用`am start -n com.example.hello_world/.MainActivity`
d.app_start("com.example.hello_world", ".MainActivity")

# 使用 `monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1` 启动,这种方法有个副作用,它自动会将手机的旋转锁定给关掉
d.app_start("com.example.hello_world", use_monkey=True)

# 启动应用前停止此应用
d.app_start("com.example.app", stop=True)

停止应用

# 等价于`am force-stop`,此方法会丢失应用数据
d.app_stop("com.example.app")

# 等价于`pm clear`
d.app_clear('com.example.hello_world')

# 停止所有应用
d.app_stop_all()

# 结束所有应用,除了excludes参数列表中的应用包名
# 如果不传参,则会只保留两个依赖服务应用
# 会返回一个结束应用的包名列表
d.app_stop_all(excludes=['com.examples.demo'])

获取 APP 信息

d.app_info('com.xueqiu.android')

#输出
{
"packageName": "com.xueqiu.android",
"mainActivity": "com.xueqiu.android.common.splash.SplashActivity",
"label": "雪球",
"versionName": "12.6.1",
"versionCode": 257,
"size": 72597243
}

获取 APP 图标

img = d.app_icon("com.examples.demo")
img.save("icon.png")

列出所有运行中的 APP

d.app_list_running()

等待 APP 启动

也可以通过 Session 来判断

# 等待应用运行, return pid(int)
pid = d.app_wait("com.example.android")

if not pid:
print("com.example.android is not running")
else:
print("com.example.android pid is %d" % pid)

# 等待应用前台运行
d.app_wait("com.example.android", front=True)
# 最长等待时间20s(默认)
d.app_wait("com.example.android", timeout=20.0)

# 或者采用如下方式
d.wait_activity(".ApiDemos", timeout=10) # default timeout 10.0 seconds

安装 APP

可以从本地路径及 url 下载安装 APP,此方法无返回值,当安装失败时,会抛出 RuntimeError 异常

# 本地路径安装
d.app_install('test.apk')
# url安装
d.app_install('http://s.toutiao.com/UsMYE/')

卸载 APP

# 卸载成功返回true,没有此包或者卸载失败返回False
d.app_uninstall('com.xueqiu.android')

# 卸载所有自己安装的第三方应用,返回卸载app的包名列表
# excludes表示不卸载的列表
# verbose为true则会打印卸载信息
d.app_uninstall_all(excludes=[],verbose=True)

卸载全部应用返回的包名列表并一定是卸载成功了,最好使用 verbose=true 打印一下信息,这样可以查看到是否卸载成功

uninstalling com.xueqiu.android  OK
uninstalling com.android.cts.verifier FAIL

Session 操作

一般用于测试某个特定的 APP,首先将某个 APP 设定为一个 Session,所有的操作都基于此 Session,当 Session 退出时,代表 APP 退出。

启动应用并获取 session

session 的用途是操作的同时监控应用是否闪退,当闪退时操作,会抛出 SessionBrokenError

sess = d.session("com.example.app")

停止或重启 session,即 app

sess.close() # 停止app
sess.restart() # 冷启app

# 开启某个APP执行某个操作后,自动退出某个session
with d.session("com.netease.cloudmusic") as sess:
sess(text="Play").click()

# APP已运行时自动跳过启动
sess = d.session("com.netease.cloudmusic", attach=True)

# 当某个APP没有启动时,报错
sess = d.session("com.netease.cloudmusic", attach=True, strict=True)

# 确定session对应的APP是否运行
# Warning: function name may change in the future
sess.running() # True or False

确定 session 对应的 APP 是否运行,当不在运行将报错

# When app is still running
sess(text="Music").click() # operation goes normal

# If app crash or quit
sess(text="Music").click() # raise SessionBrokenError
# other function calls under session will raise SessionBrokenError too

图像操作

用于获取 Android 当前的截图和界面元素。

截图

# 截图并保存到电脑上的文件,要求Android>=4.2。
d.screenshot("home.jpg")

# 获取 PIL.Image 格式的图像。 当然,你需要先安装pillow
image = d.screenshot() # default format="pillow"
# 保存为home.jpg或home.png. 目前仅支持 png 和 jpg
image.save("home.jpg")

# 获取 opencv 格式的图像。 当然,你需要先安装 numpy 和 cv2
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)

# 获取jpeg的raw数据
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)

录屏

首先需要下载依赖,官方推荐使用镜像下载

pip install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple

# 启动录制,默认帧率为20
d.screenrecord('test.mp4')
# 其它操作
time.sleep(10)
#停止录制,只有停止录制了才能看到视频
d.screenrecord.stop()

获取 hierarchy

# 获取 UI 层次结构转储内容 (unicode)。
xml = d.dump_hierarchy()

元素定位

ui2 支持 android 中 UiSelector 类中的所有定位方式,详细可以在这个网址查看 https://developer.android.com/reference/android/support/test/uiautomator/UiSelector

整体内容如下,所有的属性可以通过 weditor 查看到

名称描述
texttext 是指定文本的元素
textContainstext 中包含有指定文本的元素
textMatchestext 符合指定正则的元素
textStartsWithtext 以指定文本开头的元素
classNameclassName 是指定类名的元素
classNameMatchesclassName 类名符合指定正则的元素
descriptiondescription 是指定文本的元素
descriptionContainsdescription 中包含有指定文本的元素
descriptionMatchesdescription 符合指定正则的元素
descriptionStartsWithdescription 以指定文本开头的元素
checkable 可检查的元素,参数为 True,False
checked 已选中的元素,通常用于复选框,参数为 True,False
clickable 可点击的元素,参数为 True,False
longClickable 可长按的元素,参数为 True,False
scrollable 可滚动的元素,参数为 True,False
enabled 已激活的元素,参数为 True,False
focusable 可聚焦的元素,参数为 True,False
focused 获得了焦点的元素,参数为 True,False
selected 当前选中的元素,参数为 True,False
packageNamepackageName 为指定包名的元素
packageNameMatchespackageName 为符合正则的元素
resourceIdresourceId 为指定内容的元素
resourceIdMatchesresourceId 为符合指定正则的元素

子元素和兄弟定位

sibling()

#查找与google同一级别,类名为android.widget.ImageView的元素
d(text="Google").sibling(className="android.widget.ImageView")

链式调用

d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text("Wi‑Fi", className="android.widget.LinearLayout") \
.child(className="android.widget.Switch") \
.click()

相对定位

相对定位支持在 left, right, top, bottom, 即在某个元素的前后左右

d(A).left(B),# 选择A左边的B
d(A).right(B),# 选择A右边的B
d(A).up(B), #选择A上边的B
d(A).down(B),# 选择A下边的B
#选择 WIFI 右边的开关按钮
d(text='Wi‑Fi').right(resourceId='android:id/widget_frame')

元素常用 API

表格标注有 @property 装饰的类属性方法,均为下方示例方式

d(test="Settings").exists
方法描述返回值备注
exists() 判断元素是否存在 True,Flase@property
info() 返回元素的所有信息字典 @property
get_text() 返回元素文本字符串
set_text(text) 设置元素文本 None
clear_text() 清空元素文本 None
center() 返回元素的中心点位置(x,y) 基于整个屏幕的点

exists 其它使用方法:

d.exists(text='Wi‑Fi',timeout=5)

info () 输出信息:

{
"bounds": {
"bottom": 407,
"left": 216,
"right": 323,
"top": 342
},
"childCount": 0,
"className": "android.widget.TextView",
"contentDescription": null,
"packageName": "com.android.settings",
"resourceName": "android:id/title",
"text": "Wi‑Fi",
"visibleBounds": {
"bottom": 407,
"left": 216,
"right": 323,
"top": 342
},
"checkable": false,
"checked": false,
"clickable": false,
"enabled": true,
"focusable": false,
"focused": false,
"longClickable": false,
"scrollable": false,
"selected": false
}

可以通过上方信息分别获取元素的所有属性

XPath 定位

因为 Java uiautoamtor 中默认是不支持 xpath,这是属于 ui2 的扩展功能,速度会相比其它定位方式慢一些

在 xpath 定位中,ui2 中的 description 定位需要替换为 content-desc,resourceId 需要替换为 resource-id

# 只会返回一个元素,如果找不到元素,则会报XPathElementNotFoundError错误
# 如果找到多个元素,默认会返回第0个
d.xpath('//*[@resource-id="com.android.launcher3:id/icon"]')

# 如果返回的元素有多个,需要使用all()方法返回列表
# 使用all方法,当未找到元素时,不会报错,会返回一个空列表
d.xpath('//*[@resource-id="com.android.launcher3:id/icon"]').all()

设备交互

单击

# XY坐标
d.click(10, 20)
# XY坐标双击
d.double_click(x, y)
# 两次点击之间默认间隔0.1s
d.double_click(x, y, 0.1)

# 单击元素中心点
d(text="Settings").click()
# 双击元素中心点
d(Text="Settings").double_click()
# 长按元素中心点
d(Text="Settings").long_click()

# 等待元素出现然后单击,超时默认10s
d(text="Settings").click(timeout=10)

# click with offset(x_offset, y_offset)
# click_x = x_offset * width + x_left_top
# click_y = y_offset * height + y_left_top
d(text="Settings").click(offset=(0.5, 0.5)) # 默认点击中心
d(text="Settings").click(offset=(0, 0)) # 点击左上角
d(text="Settings").click(offset=(1, 1)) # 点击右下角

# 当元素存在时点击,超时默认10s
clicked = d(text='Skip').click_exists(timeout=10.0)

# 单击直到元素消失,超时时间10,点击间隔1
is_gone = d(text='Settings').click_gone(maxretry=10, interval=1.0)

长按

d.long_click(x, y)
# 长按点击,默认0.5s
d.long_click(x, y, 0.5)

滑动操作

基于坐标
# 从(10, 20)滑动到(80, 90)
d.swipe(10, 20, 80, 90)
d.swipe(sx, sy, ex, ey, 0.5)
基于元素
d(text="Settings").swipe("right")
d(text="Settings").swipe("left", steps=10)
# 在Setings上向上滑动,steps默认为10
# 1步约为5毫秒,因此20步约为0.1s
d(text="Settings").swipe("up", steps=20)
# 在Setings上向下滑动
d(text="Settings").swipe("down", steps=20)
# swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
# time will speed 0.2s bwtween two points
d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))

基于整个屏幕

# 支持前后左右的滑动
# "left", "right", "up", "down"
# 下滑操作
d.swipe_ext("down")

# 屏幕右滑,滑动距离为屏幕宽度的90%
d.swipe_ext("right", scale=0.9)

拖动

# 从一个坐标拖拽到另一个坐标
d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)

# 在0.25S内将Setting拖动至Clock上,拖动元素的中心位置
# duration默认为0.5,实际拖动的时间会比设置的要高
d(text="Settings").drag_to(text="Clock", duration=0.25)

# 拖动settings到屏幕的某个点上
d(text="Settings").drag_to(877,733, duration=0.25)

模拟按下后的连续操作

如九宫格解锁

# 模拟按下
d.touch.down(10, 10)
# down 和 move 之间的延迟,自己控制
time.sleep(.01)
# 模拟移动
d.touch.move(15, 15)
# 模拟抬起
d.touch.up()

模拟两指缩放

Android >= 4.3

# 缩小
d(text="Settings").pinch_in(percent=100, steps=10)
# 放大
d(text="Settings").pinch_out()

# 对元素操作
d(text='Settings').gesture(start1,start2,end1,end2,)
# 放大操作
d(text='Settings').gesture((525,960),(613,1121),(135,622),(882,1540))

d().pinch_in(percent=100, steps=10)
d().pinch_out()

等待元素出现或者消失

# 等待元素出现
d(text="Settings").wait(timeout=3.0)
# 等待元素消失,返回True/False,timout默认为全局设置的等待时间
d(text='Settings').wait_gone(timeout=20)

滚动界面

设置 scrollable 属性为 True

滚动类型:horiz 水平,vert 为垂直
滚动方向:forward 向前,backward 向后

  • toBeginning 滚动至开始
  • toEnd 滚动至最后
  • to 滚动直接某个元素出现

所有方法均返回 Bool 值

# 垂直滚动到页面顶部
d(scrollable=True).scroll.toBeginning()
# 横向滚动到最左侧
d(scrollable=True).scroll.horiz.toBeginning()
# 垂直滚动到页面最底部
d(scrollable=True).scroll.toEnd()
# 横向滚动到最右侧
d(scrollable=True).scroll.horiz.toEnd()
# 垂直向后滚动到指定位置
d(scrollable=True).scroll.to(description="指定位置")
# 横向向右滚动到指定位置
d(scrollable=True).scroll.horiz.to(description="指定位置")
# 垂直向前滚动(横向同理)
d(scrollable=True).scroll.forward()
# 垂直向前滚动到指定位置(横向同理)
d(scrollable=True).scroll.forward.to(description="指定位置")
# 滚动直到System元素出现
d(scrollable=True).scroll.to(text="System")

文件导入导出

导入文件

# 如果是目录,这里"/sdcrad/"最后一个斜杠一定要加,否则会报错
d.push("test.txt","/sdcrad/")
d.push("test.txt","/sdcrad/test.txt")

导出文件

d.pull('/sdcard/test.txt','text.txt')

执行 Shell 命令

执行非阻塞命令

# 返回输出和退出码,正常为0,异常为1
# output返回的是一个整体的字符串,如果需要抽取值,需要对output进行解析提取处理
output, exit_code = d.shell(["ls","-l"],timeout=60)
12

执行阻塞命令(持续执行的命令)

# 返回一个命令的数据流 output为requests.models.Response
output = d.shell('logcat',stream=True)
try:
# 按行读取,iter_lines为迭代响应数据,一次一行
for line in output.iter_lines():
print(line.decode('utf8'))
finally:
output.close()

打开通知栏与快速设置

# 打开通知栏
d.open_notification()

# 打开快速设置
d.open_quick_settings()

模拟输入

需要光标已经在输入框中才可以

# 切换成FastInputIME输入法
d.set_fastinput_ime(True)
# adb广播输入
d.send_keys("你好123abcEFG")
# 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7)
d.clear_text()
# 切换成正常的输入法
d.set_fastinput_ime(False)
# 模拟输入法的搜索
d.send_action("search")
# 查看当前输入法
d.current_ime()
# 返回值: ('com.github.uiautomator/.FastInputIME', True)

清空输入框

d.clear_text()

亮灭屏

# 亮屏
d.screen_on()
# 灭屏
d.screen_off()

屏幕方向

# 设置屏幕方向
d.set_orientation(value)
# 获取当前屏幕方向
d.orientation

value 值参考,任意一个值就可以

# 正常竖屏
(0, "natural", "n", 0),

# 往左横屏,相当于手机屏幕顺时针旋转90度
# 现实中如果要达到此效果,需要将手机逆时针旋转90度
(1, "left", "l", 90)

# 倒置,这个需要看手机系统是否支持,倒过来显示
(2, "upsidedown", "u", 180)

# 往右横屏,调整与往左相反,屏幕顺时针旋转270度
(3, "right", "r", 270)

硬按键操作

用于模拟用户对手机硬按键或系统按键的操作。

模拟按 Home 或 Back 键

目前支持以下关键字,但并非所有设备都支持:

  • home
  • back
  • left
  • right
  • up
  • down
  • center
  • menu
  • search
  • enter
  • delete ( or del)
  • recent (recent apps)
  • volume_up
  • volume_down
  • volume_mute
  • camera
  • power
d.press("back")
d.press("home")

模拟按 Android 定义的硬键值

d.press(0x07, 0x02)
# press keycode 0x07('0') with META ALT(0x02)
# 具体可查询:
# https://developer.android.com/reference/android/view/KeyEvent.html

解锁屏幕

d.unlock()
# 这相当于
# 1. 启动活动:com.github.uiautomator.ACTION_IDENTIFY
# 2. 按“home”键

参考