UiAutomator是Google提供的安卓自动化测试Java库,功能很强,但测试脚本只能使用Java语言,脚本要打包成jar或者apk包上传到设备上才能运行。
感谢 Xiaocong He (@xiaocong),他用Python编写了uiautomator,原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库。因为xiaocong/uiautomator已经很久不更新。所以我们直接fork了一个版本,命名为uiautomator2,对原有的库的bug进行了修复,还增加了很多新的Feature。主要有以下部分:
设备和测试电脑可以脱离数据线,通过WiFi互联(基于atx-agent)
集成openstf/minicap达到实时屏幕投频,以及实时截图
集成openstf/minitouch达到精确实时控制设备
修复xiaocong/uiautomator经常性退出的问题
代码进行了重构和精简,方便维护
实现了一个设备管理平台(也支持iOS) atxserver2
这里要先说明下,因为经常有很多人问 openatx/uiautomator2 并不支持iOS测试,需要iOS自动化测试,可以转到这个库 openatx/facebook-wda。
PS: 这个库 https://github.com/NeteaseGame/ATX 目前已经不维护了,请尽快更换。
Android Uiautomator2 Python Wrapper 这是一个可以完成Android的UI自动化的python库。
该项目还在火热的开发中,QQ群号: 499563266(加入有收费)。
# 由于uiautomator2仍在开发中,因此您必须添加‘--pre’才能安装开发版本。
pip install --pre uiautomator2
# 也可以从源代码安装
git clone https://github.com/openatx/uiautomator2
pip install -e uiautomator2
如果需要截屏,还要安装pillow
pip install pillow
电脑连接上一个手机或多个手机, 确保adb已经添加到环境变量中,执行下面的命令会自动安装本库所需要的设备端程序:uiautomator-server 、atx-agent、openstf/minicap、openstf/minitouch
# 初始化所有的已经连接到电脑的设备
python -m uiautomator2 init
有时候init也会出错,请参考手动Init
指南,安装提示success即可。
pip install -U weditor # 目前最新的稳定版为 0.1.0
Windows系统可以使用命令在桌面创建一个快捷方式 python -m weditor --shortcut
命令行启动 python -m weditor 会自动打开浏览器,输入设备的ip或者序列号,点击Connect即可。
具体参考文章:浅谈自动化测试工具python-uiautomator2
有3种连接方法:
• 通过WiFi
假如设备IP是10.0.0.1,你的电脑在同一网络
import uiautomator2 as u2
d = u2.connect('10.0.0.1') # alias for u2.connect_wifi('10.0.0.1')
print(d.info)
• 通过USB
假设设备编号为123456f(通过cmd:adb devices查询)
import uiautomator2 as u2
d = u2.connect('123456f') # alias for u2.connect_usb('123456f')
print(d.info)
• 通过ADB WiFi
import uiautomator2 as u2
d = u2.connect_adb_wifi("10.0.0.1:5555")
# Equals to
# + Shell: adb connect 10.0.0.1:5555
# + Python: u2.connect_usb("10.0.0.1:5555")
无参数调用u2.connect()函数, uiautomator2将从环境变量ANDROID_DEVICE_IP或ANDROID_SERIAL获取设备IP。如果这个环境变量为空,uiautomator将退回connect_usb,你需要确保只有一个设备连接到电脑。
其中的$device_ip代表设备的ip地址。
如需指定设备需要传入—serial,如python3 -m uiautomator2 --serial bff1234 , SubCommand为子命令(init,或者screenshot等)
1.0.3 Added: python3 -m uiautomator2可以简写为uiautomator2
uiautomator2 init
# If you need specify device to init, pass --serial
python3 -m uiautomator2 init --serial your-device-serial
$ python -m uiautomator2 screenshot screenshot.jpg
python -m uiautomator2 uninstall # 卸载一个包
python -m uiautomator2 uninstall # 卸载多个包
python -m uiautomator2 uninstall --all # 全部卸载
# 设置每次单击UI后再次单击之间延迟1.5秒
d.click_post_delay = 1.5 # 默认无延迟
# 设置默认元素等待超时(秒)
d.wait_timeout = 30.0 # 默认20.0秒
d.info
以下是可能的结果:
{
u'displayRotation': 0,
u'displaySizeDpY': 640,
u'displaySizeDpX': 360,
u'currentPackageName': u'com.android.launcher',
u'productName': u'takju',
u'displayWidth': 720,
u'sdkInt': 18,
u'displayHeight': 1184,
u'naturalOrientation': True
}
d.screen_on() # 开启 screen
d.screen_off() # 关闭screen
d.info.get('screenOn') # 要求android >= 4.4
d.press("home") # 按下home键
d.press("back") # 按下back键的常规方式
d.press(0x07, 0x02) # 按下编码 0x07('0') 和 0x02(META ALT)
您可以在Android KeyEvnet
上找到所有按键代码定义。
d.unlock()
# 1. 启动 activity: com.github.uiautomator.ACTION_IDENTIFY
# 2. 按下 "home"
d.click(x, y)
d.long_click(x, y)
d.long_click(x, y, 0.5) # long click 0.5s (default)
d.swipe(sx, sy, ex, ey)
d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
注意: click, swipe, drag 支撑百分比位置。例:
d.long_click(0.5, 0.5) #长按屏幕中心
可用的方向有:
natural 或 n
left 或l
right 或r
upsidedown 或u (不可设置)
# 检索方向,可以是"natural" 或"left" 或"right" 或"upsidedown"
orientation = d.orientation
# 警告:未在我的TT-M1通过测试
# 设定orientation(方向) 和 冻结旋转.
d.set_orientation("n") # 或"natural"
d.set_orientation("l") # 或"left"
d.set_orientation("r") # 或"right"
d.set_orientation("n") # or "natural"
# 冻结旋转
d.freeze_rotation()
# 取消冻结旋转
d.freeze_rotation(False)
# 截图并保存到本地文件“home.jpg”.
d.screenshot("home.jpg")
# 获取PIL.Image 格式,需要先安装pillow
image = d.screenshot()
image.save("home.jpg") # 或 home.png
# 获取opencv 格式, 需要先安装numpy 和cv2
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
# 获取转储的内容(unicode).
xml = d.dump_hierarchy()
d.open_notification()
d.open_quick_settings()
#推送到一个文件夹
d.push("foo.txt", "/sdcard/")
# 推送并重命名
d.push("foo.txt", "/sdcard/bar.txt")
# 推送fileobj
with open("foo.txt", 'rb') as f:
d.push(f, "/sdcard/")
# 推送并修改文件模式
d.push("foo.sh", "/data/local/tmp/", mode=0o755)
d.pull("/sdcard/tmp.txt", "tmp.txt")
# 设备中没有文件会引发FileNotFoundErr
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")
包括APP安装,启动和停止
目前仅支持从url安装。
d.app_install(' HTTP://some-domain.com/some.apk ')
d.app_start("com.example.hello_world") # 以package name启动
# 执行强制停止
d.app_stop("com.example.hello_world")
# 执行应用清除
d.app_clear('com.example.hello_world')
# 停止所有
d.app_stop_all()
#停止除com.examples.demo以外的所有应用程序
d.app_stop_all(excludes=['com.examples.demo'])
选择器用于识别当前窗口中的特定ui对象.
# 要选text是'Clock',className 是'android.widget.TextView' 的元素
d(text='Clock', className='android.widget.TextView')
选择器支持以下参数. 详见UiSelecor java doc
.
• text, textContains, textMatches, textStartsWith
• className, classNameMatches
• description, descriptionContains, descriptionMatches, descriptionStartsWith
• checkable, checked, clickable, longClickable
• scrollable, enabled,focusable, focused, selected
• packageName, packageNameMatches
• resourceId, resourceIdMatches
• index, instance
# 获取 child 或 grandchild(递归获取)
d(className="android.widget.ListView").child(text="Bluetooth")
# 获取 sibling 或 child of sibling
d(text="Google").sibling(className="android.widget.ImageView")
# 获取儿孙中符合类名"android.widget.LinearLayout",text包含"Bluetooth"
d(className="android.widget.ListView", resourceId="android:id/list")\ .child_by_text("Bluetooth", className="android.widget.LinearLayout")
# 允许页面滚动搜索获得儿孙对象
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text(
"Bluetooth",
allow_scroll_search=True,
className="android.widget.LinearLayout"
)
o child_by_description 用于获取儿孙中包含指定描述的对象, 其余和child_by_text相同.
o child_by_instance 用于获取屏幕上可见的儿孙对象中的特定实例.
详细信息,请参见以下链接:
o UiScrollable
, getChildByDescription, getChildByText, getChildByInstance
o UiCollection
, getChildByDescription, getChildByText, getChildByInstance
上面的方法支持链式调用,例如,对于以下层次结构
...
我们要单击“Wi-Fi”右侧的开关来打开Wi-Fi。由于几个开关具有几乎相同的属性,因此我们不能使用像
d(className="android.widget.Switch")来选择对象,可以使用下面的代码来选。
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.
o d(A).left(B), 表示选择A左侧的B.
o d(A).right(B), 表示选择A右侧的B.
o d(A).up(B), 表示选择A上方的B.
o d(A).down(B), 表示选择在A下面的B.
因此,对于“Wi-Fi”开关,我们可以编写如下代码:
# 选择"Wi Fi"右侧的"switch"
d(text="Wi Fi").right(className="android.widget.Switch").click()
有时屏幕上有包含相同特点(例如文本)的多个对象,那么您将不得不在选择器中使用“instance”属性,如下所示:
d(text="Add new", instance=0) # 获取第一个带有文本“Add new”的元素对象
uiautomator也提供了类似列表的方法来处理类似的元素对象.
# 获取当前屏幕上带有文本“Add new”的元素的总数
d(text="Add new").count
# len函数与count属性功能相同
len(d(text="Add new"))
# 通过index获取元素实例
d(text="Add new")[0]
d(text="Add new")[1]
...
# 迭代
for view in d(text="Add new"):
view.info # ...
注意: 使用选择器(如列表)时,必须确保屏幕保持不变,否则可能会出现ui not found error.
d(text="Settings").exists # 如果存在,则为True ,否则为 False
d.exists(text="Settings") # 以上属性的别名.
d(text="Settings").info
以下是可能的结果:
{ u'contentDescription': u'',
u'checked': False,
u'scrollable': False,
u'text': u'Settings',
u'packageName': u'com.android.launcher',
u'selected': False,
u'enabled': True,
u'bounds': {u'top': 385,
u'right': 360,
u'bottom': 585,
u'left': 200},
u'className': u'android.widget.TextView',
u'focused': False,
u'focusable': True,
u'clickable': True,
u'chileCount': 0,
u'longClickable': True,
u'visibleBounds': {u'top': 385,
u'right': 360,
u'bottom': 585,
u'left': 200},
u'checkable': False
}
d(text="Settings").clear_text() # 清除文本
d(text="Settings").set_text("My text...") # 设置文本
# 点击UI对象的中心
d(text="Settings").click()
# 最长等待(元素显示)10秒,并单击(默认值),超时没有显示会报错
d(text="Settings").click(timeout=10)
# click别名,键盘操作的短名
d(text="Settings").tap()
# 不等element show
d(text="Settings").tap_nowait()
# 长按指定ui object
d(text="Settings").long_click()
将对象拖到定点或另一个对象
# 注意:在Android 4.3之前无法设置拖动.
# 拖动ui object到point (x, y)
d(text="Settings").drag_to(x, y, duration=0.5)
# 拖动ui object 到另一个ui object(中心)
d(text="Settings").drag_to(text="Clock", duration=0.25)
d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2)) # s起点,e终点
支持两种手势:
o In, 从边缘到中心
o Out, 从中心到边缘
# 注意:在Android 4.3之前无法设置缩放.
# 从边缘到中心。
d(text="Settings").pinch_in(percent=100, steps=10)
# 从中心到边缘
d(text="Settings").pinch_out()
# 等待ui对象出现
d(text="Settings").wait(timeout=3.0) # return bool
# 等待ui object消失
d(text="Settings").wait_gone(timeout=1.0)
默认超时20s. 详见"1.全局设定"
Fling(飞滑)一般用于水平或垂直翻页,可能的属性:
o horiz 或 vert
o forward 或 backward 或 toBeginning 或 toEnd
# fling默认垂直向前
d(scrollable=True).fling()
# fling水平向前
d(scrollable=True).fling.horiz.forward()
# fling垂直向后
d(scrollable=True).fling.vert.backward()
# 垂直fling到开始
d(scrollable=True).fling.h或iz.toBeginning(max_swipes=1000)
# 垂直fling到末尾
d(scrollable=True).fling.toEnd()
可能的属性:
o horiz 或 vert
o forward 或 backward 或 toBeginning 或 toEnd, 或 to
# scroll 默认垂直向前
d(scrollable=True).scroll(steps=10)
# scroll水平向前
d(scrollable=True).scroll.h或iz.forward(steps=100)
# scroll垂直向后
d(scrollable=True).scroll.vert.backward()
# 水平scroll到开始
d(scrollable=True).scroll.h或iz.toBeginning(steps=100, max_swipes=1000)
# 垂直scroll到末尾
d(scrollable=True).scroll.toEnd()
# scroll 向前垂直,直到出现指定ui object
d(scrollable=True).scroll.to(text="Security")
你可以注册触发器来执行选择器匹配不到的ui对象动作。
当选择器找不到匹配项时,uiautomator将运行所有已注册的触发器.
d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
.click(text="Force Close")
# d.watcher(name) # 创建并命名一个触发器.
# .when(condition) # 触发条件.
# .click(target) # 对目标对象执行click 动作.
d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
.press("back", "home")
# d.watcher(name) # 创建并命名一个触发器.
# .when(condition) # 触发条件.
# .press(, ..., .() # 依次按键.
一个Watcher被触发过, 意味着所有触发条件都匹配,Watcher程序已运行.
d.watcher("watcher_name").triggered # 触发过为true ,否则为false
# 删除触发器
d.watcher("watcher_name").remove()
d.watchers # 返回已注册触发器的列表
d.watchers.triggered # 任意触发器触发过为true ,否则为false
# 重置所有触发过的触发器, 之后d.watchers.triggered 返回 False.
d.watchers.reset()
# 删除所有注册的触发器
d.watchers.remove()
# 删除指定触发器, 效果和d.watcher("watcher_name").remove()相同
d.watchers.remove("watcher_name")
# 强制运行所有注册过的触发器
d.watchers.run()
另外本文还有很多没写,推荐直接去看源码init.py
通常用于不知道控件的情况下的输入。第一步切换输入法,然后发送adb广播命令,具体使用方法如下
d.set_fastinput_ime(True) # 切换成FastInputIME输入法
d.send_keys("你好123abcEFG") # adb广播输入
d.clear_text() # 清除输入框所有内容(要求 android-Uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切换成正常的输入法
显示Toast消息
d.toast.show("Hello world")
d.toast.show("Hello world", 1.0) # show for 1.0s, default 1.0s
获取Toast
# [Args]
# 5.0: max wait timeout. Default 10.0
# 10.0: cache time. return cache toast if already toast already show up in recent 10 seconds. Default 10.0 (Maybe change in the furture)
# "default message": return if no toast finally get. Default None
d.toast.get_message(5.0, 10.0, "default message")
# common usage
assert "Short message" in d.toast.get_message(5.0, default="")
# clear cached toast
d.toast.reset()
# Now d.toast.get_message(0) is None
举例: 某节点内容
<android.widget.TextView
index="2"
text="05:19"
resource-id="com.netease.cloudmusic:id/qf"
package="com.netease.cloudmusic"
content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true"
bounds="[957,1602][1020,1636]" />
xpath定位和使用方法
有些属性的名字有修改需要注意
description -> content-desc
resourceId -> resource-id
常见用法
# wait exists 10s
d.xpath("//android.widget.TextView").wait(10.0)
# find and click
d.xpath("//*[@content-desc='分享']").click()
# check exists
if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
print("exists")
# get all text-view text, attrib and center point
for elem in d.xpath("//android.widget.TextView").all():
print("Text:", elem.text)
# Dictionary eg:
# {'index': '1', 'text': '999+', 'resource-id': 'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false', 'enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false', 'long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}
print("Attrib:", elem.attrib)
# Coordinate eg: (100, 200)
print("Position:", elem.center())
其他XPath常见用法,见: https://github.com/openatx/uiautomator2/blob/master/uiautomator2/ext/xpath/README.md
很多没写在这个地方的,都放到了这里 Common Issues
停止UiAutomator守护服务
https://github.com/openatx/uiautomator2/wiki/Common-issues
因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator。下面就说下两种关闭方法
方法1:
直接打开uiautomator app(init成功后,就会安装上的),点击关闭UIAutomator
方法2:
d.service("uiautomator").stop()
# d.service("uiautomator").start() # 启动
# d.service("uiautomator").running() # 是否在运行
ATX与Maxim共存AccessibilityService的方法
https://www.cnblogs.com/insist8089/p/6898181.html
尝试手机连接PC,然后运行下面的命令
adb shell am instrument -w -r -e debug false -e class com.github.Uiautomator.stub.Stub \
com.github.Uiautomator.test/android.support.test.runner.AndroidJUnitRunner
如果运行正常,启动测试之前增加一行代码
d.healthcheck()
如果报错,可能是缺少某个apk没有安装,使用下面的命令重新初始化
python -m Uiautomator2 init --reinstall
手机python -m Uiautomator2 init之后,浏览器输入 <手机IP:7912>,会发现一个远程控制功能,延迟非常低噢。_
项目重构自 https://github.com/openatx/atx-Uiautomator
由pbr自动生成: CHANGELOG
重大更新
• Uiautomator守护程序 https://github.com/openatx/atx-agent
• Uiautomator jsonrpc server https://github.com/openatx/android-Uiautomator-server/
• codeskyblue (@codeskyblue)
• Xiaocong He (@xiaocong)
• Yuanyuan Zou (@yuanyuan)
• Qian Jin (@QianJin2013)
• Xu Jingjie (@xiscoxu)
• Xia Mingyuan (@mingyuan-xia)
• Artem Iglikov, Google Inc. (@artikz)
其他贡献者
Under MIT