简介
uiautomator2 是一个自动化测试开源工具,仅支持 android 平台的自动化测试,其封装了谷歌自带的 uiautomator2 测试框架,可以运行在支持 Python 的任一系统上,目前版本为 2.10.2
开源库地址:https://github.com/openatx/uiautomator2
工作原理

如图所示,python-uiautomator2 主要分为两个部分,python 客户端,移动设备
- python 端:运行脚本,并向移动设备发送 HTTP 请求
- 移动设备:移动设备上运行了封装了 uiautomator2 的 HTTP 服务,解析收到的请求,并转化成 uiautomator2 的代码。
整个过程
- 在移动设备上安装 atx-agent (守护进程), 随后 atx-agent 启动 uiautomator2 服务 (默认 7912 端口) 进行监听
- 在 PC 上编写测试脚本并执行(相当于发送 HTTP 请求到移动设备的 server 端)
- 移动设备通过 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 安装
查看安装是否成功
weditor --help 出现如下信息表示安装成功
|
运行 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 信息
{ "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,内存等
{ "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 }
|
获取屏幕分辨率
获取 IP 地址
Driver 全局设置
settings
查看 settings 默认设置
d.settings
{ 'operation_delay': (0, 3), 'operation_delay_methods': ['click', 'swipe'], 'wait_timeout': 20.0, 'xpath_debug': False }
|
修改默认设置,只需要修改 settings 字典即可
d.settings['operation_delay'] = (2,4.5)
d.settings['operation_delay_methods'] = {'click','press','send_keys'}
d.settings['wait_timeout'] = 10
|
使用方法或者属性设置
http 默认请求超时时间
当设备掉线时,等待设备在线时长
d.WAIT_FOR_DEVICE_TIMEOUT = 70
|
元素查找默认等待时间
打开 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
|
休眠
应用管理
获取当前界面的 APP 信息
启动应用
d.app_start("com.example.app")
d.app_start("com.example.hello_world", ".MainActivity")
d.app_start("com.example.hello_world", use_monkey=True)
d.app_start("com.example.app", stop=True)
|
停止应用
d.app_stop("com.example.app")
d.app_clear('com.example.hello_world')
d.app_stop_all()
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
等待 APP 启动
也可以通过 Session 来判断
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)
d.app_wait("com.example.android", timeout=20.0)
d.wait_activity(".ApiDemos", timeout=10)
|
安装 APP
可以从本地路径及 url 下载安装 APP,此方法无返回值,当安装失败时,会抛出 RuntimeError 异常
d.app_install('test.apk')
d.app_install('http://s.toutiao.com/UsMYE/')
|
卸载 APP
d.app_uninstall('com.xueqiu.android')
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() sess.restart()
with d.session("com.netease.cloudmusic") as sess: sess(text="Play").click()
sess = d.session("com.netease.cloudmusic", attach=True)
sess = d.session("com.netease.cloudmusic", attach=True, strict=True)
sess.running()
|
确定 session 对应的 APP 是否运行,当不在运行将报错
sess(text="Music").click()
sess(text="Music").click()
|
图像操作
用于获取 Android 当前的截图和界面元素。
截图
d.screenshot("home.jpg")
image = d.screenshot()
image.save("home.jpg")
import cv2 image = d.screenshot(format='opencv') cv2.imwrite('home.jpg', image)
imagebin = d.screenshot(format='raw') open("some.jpg", "wb").write(imagebin)
|
录屏
首先需要下载依赖,官方推荐使用镜像下载
pip install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
d.screenrecord('test.mp4')
time.sleep(10)
d.screenrecord.stop()
|
获取 hierarchy
元素定位
ui2 支持 android 中 UiSelector 类中的所有定位方式,详细可以在这个网址查看 https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
整体内容如下,所有的属性可以通过 weditor 查看到
名称 | 描述 |
---|
text | text 是指定文本的元素 |
textContains | text 中包含有指定文本的元素 |
textMatches | text 符合指定正则的元素 |
textStartsWith | text 以指定文本开头的元素 |
className | className 是指定类名的元素 |
classNameMatches | className 类名符合指定正则的元素 |
description | description 是指定文本的元素 |
descriptionContains | description 中包含有指定文本的元素 |
descriptionMatches | description 符合指定正则的元素 |
descriptionStartsWith | description 以指定文本开头的元素 |
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 |
packageName | packageName 为指定包名的元素 |
packageNameMatches | packageName 为符合正则的元素 |
resourceId | resourceId 为指定内容的元素 |
resourceIdMatches | resourceId 为符合指定正则的元素 |
子元素和兄弟定位
sibling()
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), d(A).right(B), d(A).up(B), d(A).down(B),
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
d.xpath('//*[@resource-id="com.android.launcher3:id/icon"]')
d.xpath('//*[@resource-id="com.android.launcher3:id/icon"]').all()
|
设备交互
单击
d.click(10, 20)
d.double_click(x, y)
d.double_click(x, y, 0.1)
d(text="Settings").click()
d(Text="Settings").double_click()
d(Text="Settings").long_click()
d(text="Settings").click(timeout=10)
d(text="Settings").click(offset=(0.5, 0.5)) d(text="Settings").click(offset=(0, 0)) d(text="Settings").click(offset=(1, 1))
clicked = d(text='Skip').click_exists(timeout=10.0)
is_gone = d(text='Settings').click_gone(maxretry=10, interval=1.0)
|
长按
d.long_click(x, y)
d.long_click(x, y, 0.5)
|
滑动操作
基于坐标
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)
d(text="Settings").swipe("up", steps=20)
d(text="Settings").swipe("down", steps=20)
d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))
|
基于整个屏幕
d.swipe_ext("down")
d.swipe_ext("right", scale=0.9)
|
拖动
d.drag(sx, sy, ex, ey) d.drag(sx, sy, ex, ey, 0.5)
d(text="Settings").drag_to(text="Clock", duration=0.25)
d(text="Settings").drag_to(877,733, duration=0.25)
|
模拟按下后的连续操作
如九宫格解锁
d.touch.down(10, 10)
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)
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="指定位置")
d(scrollable=True).scroll.to(text="System")
|
文件导入导出
导入文件
d.push("test.txt","/sdcrad/") d.push("test.txt","/sdcrad/test.txt")
|
导出文件
d.pull('/sdcard/test.txt','text.txt')
|
执行 Shell 命令
执行非阻塞命令
output, exit_code = d.shell(["ls","-l"],timeout=60) 12
|
执行阻塞命令(持续执行的命令)
output = d.shell('logcat',stream=True) try: for line in output.iter_lines(): print(line.decode('utf8')) finally: output.close()
|
打开通知栏与快速设置
d.open_notification()
d.open_quick_settings()
|
模拟输入
需要光标已经在输入框中才可以
d.set_fastinput_ime(True)
d.send_keys("你好123abcEFG")
d.clear_text()
d.set_fastinput_ime(False)
d.send_action("search")
d.current_ime()
|
清空输入框
亮灭屏
d.screen_on()
d.screen_off()
|
屏幕方向
d.set_orientation(value)
d.orientation
|
value 值参考,任意一个值就可以
(0, "natural", "n", 0),
(1, "left", "l", 90)
(2, "upsidedown", "u", 180)
(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 定义的硬键值
解锁屏幕
参考