QQ交流群: 815453846 Discord: https://discord.gg/PbJhnZJKDd
有段时间没有维护这个项目了(可能有两年了),但是最近工作需要又重新研究一下Android原生自动化,当然又调研了Appium,对比下来一看,发现uiautomator2这个项目的运行速度是真的好快,从检测元素到点击,都是毫秒级的,代码也比较好理解。真是没想到以前竟然写出了这么神奇的项目,这么好的项目怎么能让它落灰呢,得好好整一整,一些垃圾代码清理清理。所以项目版本从2.x.x升级到了3.x.x
还在用2.x.x版本的用户,可以先看一下2to3 再决定是否要升级3.x.x (我个人还是非常建议升级的)
2到3毕竟是大版本升级,很多的函数删掉了。首先删掉的就是atx-agent,其次还有一堆atx-agent相关的函数。废弃的功能比如init.
各种依赖库的版本号
UiAutomator是Google提供的用来做安卓自动化测试的一个Java库,基于Accessibility服务。功能很强,可以对第三方App进行测试,获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本要打包成jar或者apk包上传到设备上才能运行。
我们希望测试逻辑能够用Python编写,能够在电脑上运行的时候就控制手机。这里要非常感谢 Xiaocong He (@xiaocong),他将这个想法实现了出来(见xiaocong/uiautomator),原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库。
因为xiaocong/uiautomator
这个库,已经很久不见更新。所以我们直接fork了一个版本,为了方便做区分我们就在后面加了个2 openatx/uiautomator2,对应的Android包源码我也fork了一份,openatx/android-uiautomator-server
除了对原有的库的bug进行了修复,还增加了很多新的Feature。主要有以下部分:
这里要先说明下,因为经常有很多人问 openatx/uiautomator2 并不支持iOS测试,需要iOS自动化测试,可以转到这个库 openatx/facebook-wda。
PS: 这个库
https://github.com/NeteaseGame/ATX目前已经不维护了,请尽快更换。
这里有一份快速参考,适合已经入门的人 QUICK REFERENCE GUIDE,欢迎多提意见。
先准备一台(不要两台)开启了开发者选项
的安卓手机,连接上电脑,确保执行adb devices
可以看到连接上的设备。
运行pip3 install -U uiautomator2
安装uiautomator2
命令行运行python
打开python交互窗口。然后将下面的命令输入到窗口中。
import uiautomator2 as u2
d = u2.connect() # connect to device
print(d.info)
这时看到类似下面的输出,就可以正式开始用我们这个库了。因为这个库功能太多,后面还有很多的内容,需要慢慢去看 ....
{'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', '
screenOn': True, 'sdkInt': 27, 'naturalOrientation': True}
另外为了保持稳定,还需要开启小黄车
的悬浮窗权限。参考文章 py-uiautomator2通过悬浮窗让服务长时间可用
一般情况下都会成功,不过也可能会有意外。可以加QQ群反馈问题(群号在最上面),群里有很多大佬可以帮你解决问题。
Thank you to all our sponsors! ?
Empty
优秀文章推荐 (欢迎QQ群里at我反馈)
成都-测试只会一点点
Installation
Connect to a device
Command line
Global settings
App management
UI automation
Contributors
LICENSE
Install uiautomator2
pip install -U uiautomator2
测试是否安装成功 uiautomator2 --help
UI Inspector
pip install uiautodev
# 启动
uiauto.dev
浏览器打开 https://uiauto.dev 查看当前设备的界面结构。
uiauto.dev
uiauto.dev 是一个独立与uiautomator2之外的一个项目,用于查看图层结构的。属于旧版项目weditor的重构版本,后续也许会收费(价格肯定物超所值),来支持当前这个项目继续维护下去。感兴趣的可以加群讨论(也包含提需求) QQ群 536481989
use serialno to connect device eg. 123456f
(seen from adb devices
)
import uiautomator2 as u2
d = u2.connect('123456f') # alias for u2.connect_usb('123456f')
print(d.info)
Serial can be passed through env-var ANDROID_SERIAL
# export ANDROID_SERIAL=123456f
d = u2.connect()
其中的$device_ip
代表设备的ip地址
如需指定设备需要传入--serial
如 python3 -m uiautomator2 --serial bff1234 <SubCommand>
, SubCommand为子命令(screenshot, current 等)
1.0.3 Added:
python3 -m uiautomator2
equals touiautomator2
screenshot: 截图
$ uiautomator2 screenshot screenshot.jpg
current: 获取当前包名和activity
$ uiautomator2 current
{
"package": "com.android.browser",
"activity": "com.uc.browser.InnerUCMobile",
"pid": 28478
}
uninstall: Uninstall app
$ uiautomator2 uninstall <package-name> # 卸载一个包
$ uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包
$ uiautomator2 uninstall --all # 全部卸载
stop: Stop app
$ uiautomator2 stop com.example.app # 停止一个app
$ uiautomator2 stop --all # 停止所有的app
doctor:
$ uiautomator2 doctor
[I 2024-04-25 19:53:36,288 __main__:101 pid:15596] uiautomator2 is OK
When python quit, the UiAutomation service also quit.
打印出代码背后的HTTP请求信息
>>> d.debug = True
>>> d.info
12:32:47.182 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "b80d3a488580be1f3e9cb3e926175310", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:54179/jsonrpc/0'
12:32:47.225 Response >>>
{"jsonrpc":"2.0","id":"b80d3a488580be1f3e9cb3e926175310","result":{"currentPackageName":"com.android.mms","displayHeight":1920,"displayRotation":0,"displaySizeDpX":360,"displaySizeDpY":640,"displayWidth":1080,"productName"
:"odin","screenOn":true,"sdkInt":25,"naturalOrientation":true}}
<<< END
设置元素查找等待时间(默认20s)
d.implicitly_wait(10.0) # 也可以通过d.settings['wait_timeout'] = 10.0 修改
d(text="Settings").click() # if Settings button not show in 10s, UiObjectNotFoundError will raised
print("wait timeout", d.implicitly_wait()) # get default implicit wait
This function will have influence on click
, long_click
, drag_to
, get_text
, set_text
, clear_text
, etc.
This part showcases how to perform app management
We only support installing an APK from a URL
d.app_install('http://some-domain.com/some.apk')
# 默认的这种方法是先通过atx-agent解析apk包的mainActivity,然后调用am start -n $package/$activity启动
d.app_start("com.example.hello_world")
# 使用 monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 启动
# 这种方法有个副作用,它自动会将手机的旋转锁定给关掉
d.app_start("com.example.hello_world", use_monkey=True) # start with package name
# 通过指定main activity的方式启动应用,等价于调用am start -n com.example.hello_world/.MainActivity
d.app_start("com.example.hello_world", ".MainActivity")
# equivalent to `am force-stop`, thus you could lose data
d.app_stop("com.example.hello_world")
# equivalent to `pm clear`
d.app_clear('com.example.hello_world')
# stop all
d.app_stop_all()
# stop all app except for com.examples.demo
d.app_stop_all(excludes=['com.examples.demo'])
d.app_info("com.examples.demo")
# expect output
#{
# "mainActivity": "com.github.uiautomator.MainActivity",
# "label": "ATX",
# "versionName": "1.1.7",
# "versionCode": 1001007,
# "size":1760809
#}
# save app icon
img = d.app_icon("com.examples.demo")
img.save("icon.png")
d.app_list_running()
# expect output
# ["com.xxxx.xxxx", "com.github.uiautomator", "xxxx"]
pid = d.app_wait("com.example.android") # 等待应用运行, return pid(int)
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) # 最长等待时间20s(默认)
Added in version 1.2.0
push a file to the device
# push to a folder
d.push("foo.txt", "/sdcard/")
# push and rename
d.push("foo.txt", "/sdcard/bar.txt")
# push fileobj
with open("foo.txt", 'rb') as f:
d.push(f, "/sdcard/")
# push and change file access mode
d.push("foo.sh", "/data/local/tmp/", mode=0o755)
pull a file from the device
d.pull("/sdcard/tmp.txt", "tmp.txt")
# FileNotFoundError will raise if the file is not found on the device
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")
# grant all the permissions
d.app_auto_grant_permissions("io.appium.android.apis")
# open scheme
d.open_url("appname://appnamehost")
# same as
# adb shell am start -a android.intent.action.VIEW -d "appname://appnamehost"
This part showcases how to perform common device operations:
Run a short-lived shell command with a timeout protection. (Default timeout 60s)
Note: timeout support require atx-agent >=0.3.3
adb_shell
function is deprecated. Use shell
instead.
Simple usage
output, exit_code = d.shell("pwd", timeout=60) # timeout 60s (Default)
# output: "/n", exit_code: 0
# Similar to command: adb shell pwd
# Since `shell` function return type is `namedtuple("ShellResponse", ("output", "exit_code"))`
# so we can do some tricks
output = d.shell("pwd").output
exit_code = d.shell("pwd").exit_code
The first argument can be list. for example
output, exit_code = d.shell(["ls", "-l"])
# output: "/....", exit_code: 0
This returns a string for stdout merged with stderr.
If the command is a blocking command, shell
will also block until the command is completed or the timeout kicks in. No partial output will be received during the execution of the command. This API is not suitable for long-running commands. The shell command given runs in a similar environment of adb shell
, which has a Linux permission level of adb
or shell
(higher than an app permission).
Run a long-running shell command (Removed)
Session represent an app lifecycle. Can be used to start app, detect app crash.
Launch and close app
sess = d.session("com.netease.cloudmusic") # start 网易云音乐
sess.close() # 停止网易云音乐
sess.restart() # 冷启动网易云音乐
Use python with
to launch and close app
with d.session("com.netease.cloudmusic") as sess:
sess(text="Play").click()
Attach to the running app
# launch app if not running, skip launch if already running
sess = d.session("com.netease.cloudmusic", attach=True)
Detect app crash
# 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
# check if session is ok.
# Warning: function name may change in the future
sess.running() # True or False
Get basic information
d.info
Below is a possible output:
{'currentPackageName': 'com.android.systemui',
'displayHeight': 1560,
'displayRotation': 0,
'displaySizeDpX': 360,
'displaySizeDpY': 780,
'displayWidth': 720,
'naturalOrientation': True,
'productName': 'ELE-AL00',
'screenOn': True,
'sdkInt': 29}
Get window size
print(d.window_size())
# device upright output example: (1080, 1920)
# device horizontal output example: (1920, 1080)
Get current app info. For some android devices, the output could be empty (see Output example 3)
print(d.app_current())
# Output example 1: {'activity': '.Client', 'package': 'com.netease.example', 'pid': 23710}
# Output example 2: {'activity': '.Client', 'package': 'com.netease.example'}
# Output example 3: {'activity': None, 'package': None}
Wait activity
d.wait_activity(".ApiDemos", timeout=10) # default timeout 10.0 seconds
# Output: true of false
Get device serial number
print(d.serial)
# output example: 74aAEDR428Z9
Get WLAN ip
print(d.wlan_ip)
# output example: 10.0.0.1 or None
Get detailed device info d.device_info
device_info
print(d.device_info)
Below is a possible output:
{'arch': 'arm64-v8a',
'brand': 'google',
'model': 'sdk_gphone64_arm64',
'sdk': 34,
'serial': 'EMULATOR34X1X19X0',
'version': 14}
Get of set clipboard content
设置粘贴板内容或获取内容
clipboard/set_clipboard
d.clipboard = 'hello-world'
# or
d.set_clipboard('hello-world', 'label')
Get clipboard content
get clipboard requires IME(com.github.uiautomator/.AdbKeyboard) call
d.set_input_ime()
before using it.
```python
# get clipboard content
print(d.clipboard)
```
Turn on/off screen
d.screen_on() # turn on the screen
d.screen_off() # turn off the screen
Get current screen status
d.info.get('screenOn') # require Android >= 4.4
Press hard/soft key
d.press("home") # press the home key, with key name
d.press("back") # press the back key, with key name
d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)
These key names are currently supported:
You can find all key code definitions at Android KeyEvnet
Unlock screen
d.unlock()
# This is equivalent to
# 1. press("power")
# 2. swipe from left-bottom to right-top
Click on the screen
d.click(x, y)
Double click
d.double_click(x, y)
d.double_click(x, y, 0.1) # default duration between two click is 0.1s
Long click on the screen
d.long_click(x, y)
d.long_click(x, y, 0.5) # long click 0.5s (default)
Swipe
d.swipe(sx, sy, ex, ey)
d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
SwipeExt 扩展功能
d.swipe_ext("right") # 手指右滑,4选1 "left", "right", "up", "down"
d.swipe_ext("right", scale=0.9) # 默认0.9, 滑动距离为屏幕宽度的90%
d.swipe_ext("right", box=(0, 0, 100, 100)) # 在 (0,0) -> (100, 100) 这个区域做滑动
# 实践发现上滑或下滑的时候,从中点开始滑动成功率会高一些
d.swipe_ext("up", scale=0.8) # 代码会vkk
# 还可以使用Direction作为参数
from uiautomator2 import Direction
d.swipe_ext(Direction.FORWARD) # 页面下翻, 等价于 d.swipe_ext("up"), 只是更好理解
d.swipe_ext(Direction.BACKWARD) # 页面上翻
d.swipe_ext(Direction.HORIZ_FORWARD) # 页面水平右翻
d.swipe_ext(Direction.HORIZ_BACKWARD) # 页面水平左翻
Drag
d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
Swipe points
# 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))
多用于九宫格解锁,提前获取到每个点的相对坐标(这里支持百分比), 更详细的使用参考这个帖子 使用u2实现九宫图案解锁
Touch and drap (Beta)
这个接口属于比较底层的原始接口,感觉并不完善,不过凑合能用。注:这个地方并不支持百分比
d.touch.down(10, 10) # 模拟按下
time.sleep(.01) # down 和 move 之间的延迟,自己控制
d.touch.move(15, 15) # 模拟移动
d.touch.up(10, 10) # 模拟抬起
Note: click, swipe, drag operations support percentage position values. Example:
d.long_click(0.5, 0.5)
means long click center of screen
Retrieve/Set device orientation
The possible orientations:
natural
or n
left
or l
right
or r
upsidedown
or u
(can not be set)# retrieve orientation. the output could be "natural" or "left" or "right" or "upsidedown"
orientation = d.orientation
# WARNING: not pass testing in my TT-M1
# set orientation and freeze rotation.
# notes: setting "upsidedown" requires Android>=4.3.
d.set_orientation('l') # or "left"
d.set_orientation("l") # or "left"
d.set_orientation("r") # or "right"
d.set_orientation("n") # or "natural"
Freeze/Un-freeze rotation
# freeze rotation
d.freeze_rotation()
# un-freeze rotation
d.freeze_rotation(False)
Take screenshot
# take screenshot and save to a file on the computer, require Android>=4.2.
d.screenshot("home.jpg")
# get PIL.Image formatted images. Naturally, you need pillow installed first
image = d.screenshot() # default format="pillow"
image.save("home.jpg") # or home.png. Currently, only png and jpg are supported
# get opencv formatted images. Naturally, you need numpy and cv2 installed first
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
# get raw jpeg data
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)
Dump UI hierarchy
# get the UI hierarchy dump content
xml = d.dump_hierarchy()
# compressed=True: include not import nodes
# pretty: format xml
# max_depth: limit xml depth, default 50
xml = d.dump_hierarchy(compressed=False, pretty=False, max_depth=50)
Open notification or quick settings
d.open_notification()
d.open_quick_settings()
Selector is a handy mechanism to identify a specific UI object in the current window.
# Select the object with text 'Clock' and its className is 'android.widget.TextView'
d(text='Clock', className='android.widget.TextView')
Selector supports below parameters. Refer to UiSelector Java doc for detailed information.
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
children
# get the children or grandchildren
d(className="android.widget.ListView").child(text="Bluetooth")
siblings