赞
踩
uiautomator2是一个自动化测试开源工具,仅支持android平台的自动化测试,其封装了谷歌自带的uiautomator2测试框架,可以运行在支持Python的任一系统上,目前版本为2.10.2
开源库地址:
https://github.com/openatx/uiautomator2
引用自https://testerhome.com/topics/11357
如图所示,python-uiautomator2主要分为两个部分,python客户端,移动设备
整个过程
使用pip安装
pip install -U uiautomator2
安装完成后,使用如下python代码查看环境是事配置成功
说明:后文中所有代码都需要导入uiautomator2库,为了简化我使用u2代替,d代表driver
- import uiautomator2 as u2
- # 连接并启动
- d = u2.connect()
- print(d.info)
- print(d.app_info(''com.signway.ds''))
能正确打印出设备信息和app信息则表示安装成功
注意:需要安装 adb 工具,并配置到系统环境变量,才能操作手机
安装有问题可以到https://github.com/openatx/uiautomator2/wiki/Common-issues这里查看一下有没有相同的问题
weditor是一款基于浏览器的UI查看器,用来帮助我们查看UI元素定位。
因为uiautomator是独占资源,所以当atx运行的时候uiautomatorviewer是不能用的,为了减少atx频繁的启停,就需要用到此工具
使用pip安装
pip install -U weditor
查看安装是否成功
weditor --help
出现如下信息表示安装成功
运行weditor
- python -m weditor
- #或者直接在命令行运行
- weditor
- d(定位方式=定位值)
- #例:
- element = d(text='Phone')
- #这里返回的是一个列表,当没找到元素时,不会报错,只会返回一个长度为0的列表
- #当找到多个元素时,会返回多个元素的列表,需要加下标再定位
- element[0].click()
- #获取元素个数
- print(element.count)
ui2支持 android 中 UiSelector 类中的所有定位方式,详细可以在这个网址查看https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
整体内容如下,所有的属性可以通过weditor查看到
text | 描述 |
---|---|
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为符合指定正则的元素 |
子元素定位
child()
- #查找类名为android.widget.ListView下的Bluetooth元素
- d(className="android.widget.ListView").child(text="Bluetooth")
- # 下面这两种方式定位有点不准确,不建议使用
- d(className="android.widget.ListView")\
- .child_by_text("Bluetooth",allow_scroll_search=True)
- d(className="android.widget.ListView").child_by_description("Bluetooth")
兄弟元素定位
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')
表格标注有@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
- }
可以通过上方信息分别获取元素的所有属性
因为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()
- d(text='Settings').click()
- #单击直到元素消失,超时时间10,点击间隔1
- d(text='Settings').click_gone(maxretry=10, interval=1.0)
d(text='Settings').longclick()
Android<4.3时不能使用拖动
- #在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)
-
- #两个点之间的拖动,从点1拖动至点2
- d.drag(x1,y1,x2,y2)
滑动有两个,一个是在driver上操作,一个是在元素上操作
元素上操作
从元素的中心向元素边缘滑动
- #在Setings上向上滑动。steps默认为10
- #1步约为5毫秒,因此20步约为0.1 s
- d(text="Settings").swipe("up", steps=20)
driver上操作
即对整个屏幕操作
#实现下滑操作
- x,y = d.window_size()
- x1 = x / 2
- y1 = y * 0.1
- y2 = y * 0.9
- d.swipe(x1,y1,x1,y2)
driver滑动的扩展方法,可以直接实现滑动,不需要再自己封装定位点
- #支持前后左右的滑动
- #"left", "right", "up", "down"
- #下滑操作
- d.swipe_ext("down")
android>4.3
对元素操作
- d(text='Settings').gesture(start1,start2,end1,end2,)
- #放大操作
- d(text='Settings').gesture((525,960),(613,1121),(135,622),(882,1540))
封装好的放大缩小操作
- #缩小
- d(text="Settings").pinch_in()
- #放大
- d(text="Settings").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")
- #使用adb广播的方式输入
- d.send_keys('hello')
- #清空输入框
- d.clear_text()
两种方法
- #发送回车
- d.press('enter')
- #第二种
- d.keyevent('enter')
目前press支持的按键如下
- """
- press key via name or key code. Supported key name includes:
- home, back, left, right, up, down, center, menu, search, enter,
- delete(or del), recent(recent apps), volume_up, volume_down,
- volume_mute, camera, power.
- """
keyevent是通过“adb shell input keyevent”方式输入,支持按键更加丰富
更多详细的按键信息https://developer.android.com/reference/android/view/KeyEvent.html
- #切换成ui2的输入法,这里会隐藏掉系统原本的输入法,默认是使用系统输入法
- #当传入False时会使用系统默认输入法,默认为Fasle
- d.set_fastinput_ime(True)
- #查看当前输入法
- d.current_ime()
- #返回值
- ('com.github.uiautomator/.FastInputIME', True)
可以模拟的功能有 go ,search ,send ,next, done ,previous
如果使用press输入按键无效,可以尝试使用此方法输入
- #搜索功能
- d.send_action("search")
- #获取toast,当没有找到toast消息时,返回default内容
- d.toast.get_message(timout=5,default='no toast')
- #清空toast缓存
- d.toast.reset()
使用wather进行界面的监控,可以用来实现跳过测试过程中的弹框
当启动wather时,会新建一个线程进行监控
可以添加多个watcher
用法
- #注册监控,当界面内出现有allow字样时,点击allow
- d.watcher.when('allow').click()
-
- #移除 allow 的监控
- d.watcher.remove("allow")
-
- #移除所有的监控
- d.watcher.remove()
-
- #开始后台监控
- d.watcher.start()
- d.watcher.start(2.0) # 默认监控间隔2.0s
-
- #强制运行所有监控
- d.watcher.run()
-
- #停止监控
- d.watcher.stop()
-
- #停止并移除所有的监控,常用于初始化
- d.watcher.reset()
这里可以用来实现图案解锁
使用touch类
- #模拟按下不放手
- touch.down(x,y)
- #停住3S
- touch.sleep(x,y)
- #模拟移动
- touch.move(x,y)
- #模拟放开
- touch.up(x,y)
- #实现长按,同一个点按下休眠5S后抬起
- d.touch.down(252,1151).sleep(5).up(252,1151)
- #实现四点的图案解锁,目前只支持坐标点
- d.touch.down(252,1151).move(559,1431).move(804,1674).move(558,1666).up(558,1666)
d.screenshot('test.png')
这个感觉是比较有用的一个功能,可以在测试用例开始时录制,结束时停止录制,然后如果测试fail。则上传到测试报告,完美复原操作现场,具体原理后面再去研究
首先需要下载依赖,官方推荐使用镜像下载
pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
执行录制
- #启动录制,默认帧率为20
- d.screenrecord('test.mp4')
- #其它操作
- time.sleep(10)
- #停止录制,只有停止录制了才能看到视频
- d.screenrecord.stop()
下载与录制视频同一套依赖
这个功能是首先手动截取需要点击目标的图片,然后ui2在界面中去匹配这个图片,目前我尝试了精确试不是很高,误点率非常高,不建议使用
- #点击
- d.image.click('test.png')
- #匹配图片,返回相似度和坐标
- #{'similarity': 0.9314796328544617, 'point': [99, 630]}
- d.image.match('test.png')
- d.app_current()
- #返回当前界面的包名,activity及pid
- {
- "package": "com.xueqiu.android",
- "activity": ".common.MainActivity",
- "pid": 23007
- }
可以从本地路径及url下载安装APP,此方法无返回值,当安装失败时,会抛出RuntimeError异常
- #本地路径安装
- d.app_install('test.apk')
- #url安装
- d.app_install('http://s.toutiao.com/UsMYE/')
默认当应用在运行状态执行start时不会关闭应用,而是继续保持当前界面
如果需要消除前面的启动状态,则需要加stop=True参数
- #通过包名启动
- d.app_start("com.xueqiu.android",stop=True)
-
- #源码说明
- def app_start(self, package_name: str,
- activity: Optional[str]=None,
- wait: bool = False,
- stop: bool=False,
- use_monkey: bool=False):
- """ Launch application
- Args:
- package_name (str): package name
- activity (str): app activity
- stop (bool): Stop app before starting the activity. (require activity)
- use_monkey (bool): use monkey command to start app when activity is not given
- wait (bool): wait until app started. default False
- """
stop和clear的区别是结束应用使用的命令不同
stop使用的是“am force-stop”
clear使用的是“pm clear”
- #通过包名结束单个应用
- d.app_stop("com.xueqiu.android")
- d.app_clear('com.xueqiu.android')
-
- #结束所有应用,除了excludes参数列表中的应用包名
- #如果不传参,则会只保留两个依赖服务应用
- #会返回一个结束应用的包名列表
- d.app_stop_all(excludes=['com.xueqiu.android'])
- 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
- }
- img = d.app_icon('com.xueqiu.android')
- img.save('icon.png')
- #等待此应用变为当前应用,返回pid,超时未启动成功则返回0
- #front为true表示等待app成为当前app,
- #默认为false,表示只要后台有这个应用的进程就会返回PID
- d.app_wait('com.xueqiu.android',60,front=True)
- #卸载成功返回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
或者可以修改一下源码,使其只输出成功的包名,注释的为增加的代码,未注释的是源码
- def app_uninstall_all(self, excludes=[], verbose=False):
- """ Uninstall all apps """
- our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']
- output, _ = self.shell(['pm', 'list', 'packages', '-3'])
- pkgs = re.findall(r'package:([^\s]+)', output)
- pkgs = set(pkgs).difference(our_apps + excludes)
- pkgs = list(pkgs)
- # 增加一个卸载成功的列表
- #sucess_list = []
- for pkg_name in pkgs:
- if verbose:
- print("uninstalling", pkg_name, " ", end="", flush=True)
- ok = self.app_uninstall(pkg_name)
- if verbose:
- print("OK" if ok else "FAIL")
- # 增加如下语句,当成功则将包名加入list
- #if ok:
- # sucess_list.append(pkg_name)
- # 返回成功的列表
- # return sucess_list
- return pkgs
- #当PC只连接了一个设备时,可以使用此种方式
- d = u2.connect()
- #返回的是Device类,此类继承方式如下
-
- class Device(_Device, _AppMixIn, _PluginMixIn, _InputMethodMixIn, _DeprecatedMixIn):
- """ Device object """
-
-
- #for compatible with old code
- Session = Device
connect()可以使用如下其它方式进行连接
- #当PC与设备在同一网段时,可以使用IP地址和端口号通过WIFI连接,无需连接USB线
- connect("10.0.0.1:7912")
- connect("10.0.0.1") # use default 7912 port
- connect("http://10.0.0.1")
- connect("http://10.0.0.1:7912")
- #多个设备时,使用设备号指定哪一个设备
- connect("cff1123ea") # adb device serial number
- 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地址字符串,如果没有则返回None
- d.wlan_ip
查看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
- #默认值60s,
- d.HTTP_TIMEOUT = 60
- #仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
- d.WAIT_FOR_DEVICE_TIMEOUT = 70
- #打不到元素时,等待10后再报异常
- d.implicitly_wait(10.0)
- 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)
- #亮屏
- 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))
d.open_notification()
d.open_quick_settings()
- #如果是目录,这里"/sdcrad/"最后一个斜杠一定要加,否则会报错
- d.push("test.txt","/sdcrad/")
- d.push("test.txt","/sdcrad/test.txt")
d.pull('/sdcard/test.txt','text.txt')
使用shell方法执行
output返回的是一个整体的字符串,如果需要抽取值,需要对output进行解析提取处理
- #返回输出和退出码,正常为0,异常为1
- output,exit_code = d.shell(["ls","-l"],timeout=60)
- #返回一个命令的数据流 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()
源码描述
- def shell(self, cmdargs: Union[str, List[str]], stream=False, timeout=60):
- """
- Run adb shell command with arguments and return its output. Require atx-agent >=0.3.3
- Args:
- cmdargs: str or list, example: "ls -l" or ["ls", "-l"]
- timeout: seconds of command run, works on when stream is False
- stream: bool used for long running process.
- Returns:
- (output, exit_code) when stream is False
- requests.Response when stream is True, you have to close it after using
- Raises:
- RuntimeError
- For atx-agent is not support return exit code now.
- When command got something wrong, exit_code is always 1, otherwise exit_code is always 0
- """
因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator
使用代码停止
d.service("uiautomator").stop()
直接打开ATX APP(init成功后,就会安装上),点击关闭UIAutomator
False, timeout=60):
“”"
Run adb shell command with arguments and return its output. Require atx-agent >=0.3.3
- Args:
- cmdargs: str or list, example: "ls -l" or ["ls", "-l"]
- timeout: seconds of command run, works on when stream is False
- stream: bool used for long running process.
-
- Returns:
- (output, exit_code) when stream is False
- requests.Response when stream is True, you have to close it after using
-
- Raises:
- RuntimeError
-
- For atx-agent is not support return exit code now.
- When command got something wrong, exit_code is always 1, otherwise exit_code is always 0
- """
- ##8.9 session(目前已经被弃用)
-
- ##8.10 停止UI2服务
-
- 因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator
-
- **使用代码停止**
-
- ```python
- d.service("uiautomator").stop()
手动停止
直接打开ATX APP(init成功后,就会安装上),点击关闭UIAutomator
原文链接:https://blog.csdn.net/master724/article/details/107962349
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。