uiautomator2使用 安卓设备的UI自动化测试

https://www.cnblogs.com/Ronaldo-HD/p/9907747.html

众所周知,安卓单台设备的UI自动化测试已经比较完善了,有数不清的自动化框架或者工具。但是介绍多设备管理的内容并不多,当手里的手机多了之后,要做自动化测试平台,这块的东西又不得不碰,摆脱USB限制,接入WiFi,才能更加自由

浅谈自动化测试工具 python-uiautomator2 · TesterHome

安装

安装 adb

windows下载安装adb(极其简单)_adb工具_x2584179909的博客

安装 uiautomator2

GitHub - openatx/uiautomator2: Android Uiautomator2 Python Wrapper

# Since uiautomator2 is still under development, you have to add --pre to install the development version
pip install --upgrade --pre uiautomator2

# Or you can install directly from github source
git clone https://github.com/openatx/uiautomator2
pip install -e uiautomator2

在手机安装 ATX-agent

atx-agent是运行在设备上的驻守程序,go开发,用于保活设备上相关的服务

 usb调试

usb连手机,手机的【开发者选项】里 打开允许【USB调试

cmd 执行 adb devices,可以看到设备对应的 序列码,说明已连接上

cmd 执行 python -m uiautomator2 init。会在手机上安装两个APK:ATX-agent(小黄车)和 `com.github.uiautomator.test`(不可见)

这两个apk使用同一个证书签名的。 不可见的应用实际上是一个测试包,包含有所有的测试代码,核心的测试服务也是通过其启动的。 但是运行的时候,系统却需要那个小黄车一直在运行(在后台运行也可以)。一旦小黄车应用被杀,后台运行的测试服务也很快的会被杀掉。就算什么也不做,应用应用在后台,也会很快被系统回收掉。

至此已经可以用usb调试手机了,直接跳转到下面 安装weditor

adb wifi调试

如果想后续不用usb 而用wifi调试,还需要下面的步骤:

        很多地方都是写 u2.connect('192.168.0.100'),尝试后报RuntimeError: USB device XXXXX is offline

        首先,电脑和手机要在同一局域网内(连同一个wifi),此时电脑能ping到手机'192.168.0.100',但还不够。需要开启远程adb

        参考下文

https://www.cnblogs.com/lxmtx/p/16071938.html

        wifi连接adb需要tcpip连接模式,在数据线连接时就需要设定端口,使用如下命令(每台手机都需要这样设置一次,可以写个脚本批量设置)

cmd

>adb tcpip 5566

        手机与电脑连接同一局域网,比如手机连接wifi后ip为 192.168.0.102,使用adb连接手机设备(手机ip在wlan上能找到、atx-agent里也有写)

cmd

 >adb connect 192.168.0.102:5566

        此时adb devices,就能看到一个局域网设备

        后续才能用uiautomator2.connect()或weditor界面 操作手机

import uiautomator2 as u2

d = u2.connect('192.168.0.100:5566')

d.app_current()

(如果是在weditor已打开的情况下配置的adb tcpip5566,则要重启weditor后才能成功connect '绿')

        然后手机上的atx-agent才会启动。(未连接电脑前,atx-agent里面的uiautomator服务是不会启动的)


使用adb conncet数周后可能又连不上

>adb connect 192.168.2.80:5566
cannot connect to 192.168.2.80:5566: 由于目标计算机积极拒绝,无 法连接。 (10061)

这时再接usb,即解决

>adb tcpip 5566

常用adb命令:

adb kill-server //结束adb服务
adb start-server //启动adb服务
adb devices //获取adb设备列表

adb connect xxx //创建与xxx的设备连接    connect to a device via TCP/IP [default port=5555]

#如果已经kill-server,那么start-server和devices和connect的效果都是一样的,都能直接开启adb

adb disconnect xxx //断开与xxx的设备连接    disconnect from given TCP/IP device [default port=5555], or all
#当有其他进程正在使用这个连接时,断开将无效。如weditor依然在connect,或者cmd中还有d
#kill-server也没用,weditor或cmd里一有操作就会再次启动adb
[I 230717 19:47:55 page:204] Serial: android:192.168.0.100:5566
* daemon not running; starting now at tcp:5037
* daemon started successfully
[W 230717 19:47:59 __init__:218] [pid:13800] atx-agent has something wrong, auto recovering

更多adb问题见:Android adb网络连接Offline和 adb断开连接

更多的adb指令:adb help 

https://www.jianshu.com/p/63c4d5c31909

>adb forward tcp:8888 tcp:9999    #执行完该命令后,转发PC机8888端口的数据到手机的9999端口
8888

>adb forward --list            #查看一下转发是否成功,只有通过USB成功连接了手机该命令才能成功
35b14bdb tcp:8888 tcp:9999    #可以看到转发成功

>netstat -a |findstr 8888    #查看8888端口的状态
  TCP    127.0.0.1:8888         DESKTOP-IB06ARQ:0      LISTENING
#可以看到本地的8888端口是处于LISTENING状态

#确认了转发成功后,PC机作为Client端,手机作为Server端建立Socket连接,就可以进行通信了


>adb forward --remove tcp:8888    #在通信完毕后,停止转发
>adb forward --list        #再次使用adb forward --list看不到连接就是移除成功。

wifi调试

今天看Connect to a device,又试了一下ip直连,不用adb wifi。发现可以执行

u2.connect('192.168.0.100')

但后续的代码都报USB device XXXXX is offline。。。什么鬼,成功仅限于执行上面这行

还是要先 adb connect 192.168.0.100:5566才行


安装 weditor

  • weditor 类似于uiautomatorviewer,是专门为uiautomator2开发的辅助编辑器
pip install --pre --upgrade weditor

出错就

git clone https://github.com/openatx/weditor
pip install -e weditor

使用

启动 weditor

cmd >python -m weditor (or 直接>weditor)

如果不能成功启动,则重新

uiautomator2 init

weditor

会打开一个浏览器页面localhost:17310

17310这个端口是为了纪念,项目的创建日期 2017/03/10

weditor的使用:

Android后面填 用adb devices得到的 序列号 或 ip

【connect】如果没有绿色树叶图标则未连接上。

【静态】和【实时】。【实时】则weditor的监视窗口随时与手机屏幕保持一致,但hierarchy刷新会比较慢;【静态】则需要手动点击【dump hierarchy】,监视窗口才会读取手机当前屏幕,hierarchy刷新比较快

weditor使用过程中,手机的ATX要保持运行(后台即可),保活uiautomator,这样weditor的连接才不会中断。

测试代码可以在weditor编辑窗口写,也可以在cmd->python进python shell写。在weditor的好处是,输入d就有api提示。还可以点击监视窗口查看控件信息


可以直接在cmd使用的指令

不用进python shell. github.com/README.md#command-line

screenshot: 截图
$ uiautomator2 screenshot screenshot.jpg

current: 获取当前包名和activity
$ uiautomator2 current

uninstall: 卸载            #没试过
$ uiautomator2 uninstall <package-name> # 卸载一个包
$ uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包
$ uiautomator2 uninstall --all # 全部卸载

stop: 停止应用
$ uiautomator2 stop com.example.app # 停止一个app
$ uiautomator2 stop --all # 停止所有的app    #慎用

uiautomator2 api

随着版本升级,设置过期的配置时,会提示Deprecated,但是不会抛异常。

基操
>adb devices
>adb connect 192.168.2.80:5566
>weditor
# coding: utf-8
#
import uiautomator2 as u2

d = u2.connect("--serial-here--") # 只有一个设备时可以省略参数
d = u2.connect() # 一个设备时
d = u2.connect("10.1.2.3:5566") # 通过设备的IP连接(需要在同一局域网且设备上的atx-agent已经安装并启动)

#Retrieve the device info
d.info        #Get basic information, return a dictionary
d.device_info    #Get detailed information
d.info.get('screenOn')

d.window_size()    #截取屏幕尺寸    return (1080, 2340)

d.wait_activity(".ApiDemos", timeout=10)    #return True of False        
#10秒在手机当前页面找到activity就返回True.    activity可以在weditor找到

d.serial        #192.168.2.80:5566 设备序列号 或 ip
d.wlan_ip        #192.168.2.80

 New command timeout :若客户端在 设定时间内没有发出新命令, uiautomator 服务就会结束。

d.set_new_command_timeout(300) # change to 5 minutes, unit seconds
#默认3分钟
d.app_current() # 获取前台应用的 packageName, activity, pid
d.app_list_running()    #查看所有正在运行的 app
d.app_info("com.examples.demo")    #查看 app 信息

img = d.app_icon("com.tencent.mobileqq")        # save app icon
#img.save("icon.png")    #这个我也不知道图片存到哪
img.save("D:\username\icon.png")    #如果出现SyntaxError: (unicode error),换个路径试试

d.app_start('com.tencent.qqmusic') # 启动应用。不需要在当前页面寻找
#d.app_start("com.example.hello_world", ".MainActivity")
d.app_start("com.tencent.mobileqq",'.activity.SplashActivity')
d.app_start("com.example.app", stop=True) # 启动应用前 先停止应用

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(默认)

#相当于`am force-stop`,因此你可能丢失数据
d.app_stop("com.example.app") # 停止应用。无论应用在前、后台都会被停止,但 无法杀掉后台记录。
d.app_stop_all()        #这条慎用。。。系统壁纸都给我重置了,输入法也要重设默认
d.app_stop_all(excludes=['com.examples.demo'])    # stop all app except for com.examples.demo

d.app_clear("com.example.app")    #清后台 equivalent to `pm clear`

d.app_install('http://some-domain.com/some.apk')    #安装应用
#Open Scheme
#等同于在cmd直接adb shell am start -a android.intent.action.VIEW -d "appname://appnamehost"
d.open_url("https://www.baidu.com")        #系统自带浏览器打开网页
d.open_url("taobao://taobao.com") # open Taobao app
d.open_url("appname://appnamehost")
d.push('foo.txt的', '/sdcard/')    #推送文件到手机里的 文件夹
d.push('foo.txt的', '/sdcard/bar.txt')    #推送 并重命名
d.push('foo.sh', '/data/local/tmp/',mode = 0o755)    # push and change file access mode
# push fileobj
with open("foo.txt", 'rb') as f:
    d.push(f, "/sdcard/")

d.pull('/sdcard/tmp.txt', 'tmp.txt')   #从设备中拉取文件:
# FileNotFoundError will raise if the file is not found on the device
Debug HTTP requests
Trace HTTP requests and response to find out how it works.
跟踪 HTTP 请求和响应以了解其工作原理

>>> d.debug=False        #默认
>>> d.info
{'currentPackageName': 'com.android.systemui', 'displayHeight': 2182, 'displayRotation': 0, 'displaySizeDpX': 384, 'displaySizeDpY': 832, 'displayWidth': 1080, 'productName': 'OnePlus7Pro_CH', 'screenOn': False, 'sdkInt': 30, 'naturalOrientation': True}

>>> d.debug = True        #开启debug,会获得每次的http信息
>>> d.info
22:51:54.997 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "6b3c790115b09ee646131b1fb1eb1f33", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:56116/jsonrpc/0'
22:51:56.534 Response (1536 ms) >>>
{"jsonrpc":"2.0","id":"6b3c790115b09ee646131b1fb1eb1f33","result":{"currentPackageName":"com.android.systemui","displayHeight":2182,"displayRotation":0,"displaySizeDpX":384,"displaySizeDpY":832,"displayWidth":1080,"productName":"OnePlus7Pro_CH","screenOn":false,"sdkInt":30,"naturalOrientation":true}}
<<< END
{'currentPackageName': 'com.android.systemui', 'displayHeight': 2182, 'displayRotation': 0, 'displaySizeDpX': 384, 'displaySizeDpY': 832, 'displayWidth': 1080, 'productName': 'OnePlus7Pro_CH', 'screenOn': False, 'sdkInt': 30, 'naturalOrientation': True}
'''
Set default element wait time, unit seconds 设置元素查找等待时间(默认20s)
This function will have influence on click, long_click, drag_to, get_text, set_text, clear_text, etc.
'''

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
d.set_clipboard('text', 'label')    #有第一个参数就够了。就是要复制到剪贴板的内容
d.clipboard        #查看剪贴板的内容

d.screen_on()       #打开屏幕
d.screen_off()        #关闭屏幕
d.unlock()    #解锁屏幕,源码就是power键+右滑,所以有锁的手机并不能解锁

d.press("back") # 模拟点击返回键
d.press("home") # 模拟Home键

'''以下功能不同手机有区别
power    锁屏键
volume_up
volume_down
volume_mute        直接静音
菜单键: menu        一加是到壁纸设置的界面
left、right、up、down    方向键可以控制写大段落文字时的光标,也可以切换app内的界面,比如微信
enter    回车
delete ( or del)    等于 退格键backspace
recent (recent apps)    查看后台软件
camera     一加手机无反应

def press(self, key: Union[int, str], meta=None):   源码在uiautomator2/__init__.py#L1152
即press能接受str或int,  还能接受int+meta键同时按下,meta就是功能键,alt之类的
'''
#int即软键盘(拨号、打字时弹出的软键盘)上的字符 对应的keycode
#keycode查询:https://developer.android.com/reference/android/view/KeyEvent.html#META_ALT_ON

d.press(0x07, 0x02)     # press keycode 0x07('0') with ALT(0x02),只有在拨号时能输出0,打字时无效
d.press(0x07)    #拨号、打字时都能输出0
#我也不知道加alt有什么用。。。? 可能是为全键盘手机考虑的吧
#Gesture interaction with the device
d.click(x, y)
d.click(10, 20)    #像素
d.click(0.9, 0.1)    #整个屏幕的占比

d.double_click(x, y)
d.double_click(x, y, 0.1) # default duration between two click is 0.1s

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.swipe(10, 20, 80, 90) # 从(10, 20)滑动到(80, 90)
#滑动解锁
d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))    #swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
d.swipe_points([(0.501, 0.333),(0.278, 0.437),(0.516, 0.556),(0.739, 0.448),(0.514, 0.448)], 0.2)    #0.2s

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是对sxsy处的app进行拖动(如果sxsy刚好有app),拖动到exey位置;swipe只是单纯的滑动。
d.drag(sx, sy, ex, ey)    #同样支持百分比 和 像素
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)

#Touch and drap (Beta) 
#这个接口属于比较底层的原始接口
d.touch.down(10, 10) # 模拟按下
time.sleep(.01) # down 和 move 之间的延迟,自己控制
d.touch.move(15, 15) # 模拟移动
d.touch.up() # 模拟抬起
#滑动解锁 全程
d.screen_on()
d.swipe_ext("right", 0.6)
d.swipe_ext("up", 0.6)
d.swipe_points([(0.501, 0.333),(0.278, 0.437),(0.516, 0.556),(0.739, 0.448),(0.514, 0.448)], 0.2) 
Stop UiAutomator

直接在小黄车 停止服务,        或者用下面的命令

#停用UiAutomator的守护程序 
d.uiautomator.stop()
#d.service("uiautomator").stop()    #DeprecationWarning: Call to deprecated method service. (You should use d.uiautomator.start() instead)
#这会关闭atx-agent,atx里刷新服务状态会看到{'running':False}
#在weditor里connect或者任意操作,就能重启atx-agent并恢复uiautomator服务

# d.uiautomator.start() # 启动
# d.uiautomator.running() # 是否在运行
Screen-related
#检索/设置设备方向
orientation = d.orientation
print(orientation)

# set orientation and freeze rotation.    忘记上次用什么app实现转向的了
# notes: setting "upsidedown" requires Android>=4.3.
d.set_orientation('n') # or "natural "
d.set_orientation("l") # or "left"
d.set_orientation("r") # or "right"
d.set_orientation("u") # or "upsidedown" (can not be set)
#关闭、开启 屏幕自动跟随旋转
# freeze rotation
d.freeze_rotation()    #关闭 旋转
# un-freeze rotation
d.freeze_rotation(False)    #开启 旋转
#截图,默认返回pillow格式的图片
    # 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 转储 UI 层次结构
# get the UI hierarchy dump content (unicoded).
xml = d.dump_hierarchy()
#下拉菜单
d.open_notification()    #只下拉出第一行
d.open_quick_settings()    #下拉出第一页
Selector 

选择器是一种方便的机制,用于识别当前窗口中的特定 UI 对象

#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](http://developer.android.com/tools/help/uiautomator/UiSelector.html) 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 and siblings

# get the children or grandchildren    后代元素
d(className="android.widget.ListView").child(text="Bluetooth")    #text一定要完全匹配
d(className="android.view.ViewGroup").child(description="vip歌曲").click()
# 多个参数 指定父元素和子元素
d(className="android.widget.LinearLayout", resourceId="com.tencent.mm:id/kp3").child(text="朋友圈", className="android.widget.TextView").click()

# 通过父 和 孙(后代),去找子
d(父属性*n).child_by_text(孙(后代)的text, 子属性*n).click()     #返回子
d(className="android.widget.LinearLayout").child_by_text("我的快递", className="android.widget.RelativeLayout").click()        #当页面有很多属性符合的父和子时,可能会找不到。 这时需要添加更多的属性
d(className="android.widget.LinearLayout").child_by_text("我的快递", className="android.widget.RelativeLayout", resourceId="com.alipay.android.phone.openplatform:id/home_app_view").click()    #给子添加resourceId属性,就能找到了
d(className="android.widget.LinearLayout").child_by_text("消息盒子", className="android.widget.LinearLayout", resourceId="com.alipay.mobile.homefeeds:id/notification_msg_box").click()    #同上
#用setting和wechat的界面 测试child_by_text时,因为可用的 子属性少,总是返回页面上text对不上的元素,原因未查明?
#网上别人uiautomator用法d().child_by_text().siblings()
#我uiautomator2用会报错'str' object has no attribute 'clone'
#有人提了issue,作者暂时没空修

#还可以滚动屏幕来寻找 allow_scroll_search=True
d(className="android.widget.ScrollView").child_by_text("工具箱", allow_scroll_search=True, className="android.widget.LinearLayout").click()
#又出现了选中其他元素的情况。。。总感觉child_by_text不怎么好用

# get siblings    同级元素
d(text="Google").sibling(className="android.widget.ImageView")
d(text="支付宝").sibling(description="时钟").click()
d(description="K歌").sibling(description="下载").click()

#relative positioning    用相对位置来找元素
d(text="Wi‑Fi").right(className="android.widget.Switch").click()    #left right up down

#Multiple instances    当找到很多个元素时,指定要第几个
d(text="Add new", instance=0)  # 计数从0开始,要第1个
d(className="android.widget.TextView")[2].click()   #要第3个
#元素相关的其他api
# get the count of views with text "Add new" on current screen
d(text="Add new").count
len(d(text="Add new"))    # same as count property ,不是列表,但 是可迭代对象

# get the instance via index
d(text="Add new")[0]
d(text="Add new")[1]
...

# iterator
for view in d(text="Add new"):
    view.info  # ...
 Get the selected ui object status and its information
d(text="Settings").exists # True if exists, else False
d.exists(text="Settings") # alias of above property.
# advanced usage
d(text="Settings").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)

d(text="Settings").info    #Retrieve the info of the specific UI object

#Get/Set/Clear text of an editable field (e.g., EditText widgets)
d(text="Settings").get_text()  # get widget text 有text的元素都能获得
d(className="android.widget.MultiAutoCompleteTextView").set_text("My text...")  # set the text 这个不是app改名,在widget.Text...的地方才能输入
d(className="android.widget.EditText")[0].clear_text()  # 在widget.Text...的地方才能清除 

#对非widget.Text...的元素,在光标处输入
d.set_fastinput_ime(True) # 切换成FastInputIME输入法
d.send_keys("你好123abcEFG") # adb广播输入
d.clear_text() # 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切换成 系统自带的输入法(不是自己另外装的)


d.send_action("search") # 该函数可以使用的参数有 go search send next done previous    
#什么时候该使用这个函数呢?
#有些时候在EditText中输入完内容之后,调用press("search") or press("enter")发现并没有什么反应。 
#这个时候就需要send_action函数了,这里用到了只有输入法才能用的IME_ACTION_CODE。 
#send_action先broadcast命令发送给输入法操作IME_ACTION_CODE,由输入法完成后续跟EditText的通信。
x, y = d(text="Settings").center()    #Get Widget center point

im = d(text="支付宝").screenshot()        #将整个元素框截下来
im.save("D:\你的名字\settings.jpg")    #不知道为什么保存到桌面会报错,明明都是全英
 click action on the selected UI object
d(text="Settings").click()    # click on the center of the specific ui object
#自定义点击元素的位置,从元素框的左上角开始,写比例会自动计算
d(text="Settings").click(offset=(0.5, 0.5)) # Default center
d(text="Settings").click(offset=(0, 0)) # click left-top
d(text="Settings").click(offset=(1, 1)) # click right-bottom

# wait element to appear for at most 10 seconds and then click
d(text="Settings").click(timeout=10)    #等10s超时后报错
clicked = d(text='Skip').click_exists(timeout=10.0)    #同上,区别在10s内有返回True,没有返回False,不报错

# click until element gone, return bool 一直点,直到页面内没有这个元素,返回True,如果元素还在,返False
is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0
#点10次,每次间隔1s

# wait until the ui object appears
d(text="Settings").wait(timeout=3.0) # return bool    The default timeout is 20s.
# wait until the ui object gone
d(text="Settings").wait_gone(timeout=1.0)
Gesture actions for the specific UI object
# notes : drag can not be used for Android<4.3.
# drag the UI object to a screen point (x, y), in 0.5 second
d(text="Settings").drag_to(x, y, duration=0.5)
# drag the UI object to (the center position of) another UI object, in 0.25 second
d(text="Settings").drag_to(text="Clock", duration=0.25)

#从元素的中心滑动到其边缘
d(text="Settings").swipe("up", steps=20) # 1 steps is about 5ms 当steps过短时,就和滑动屏幕一个效果 
#up right left down

#Two-point gesture from one point to another 两点操作,比如放大图片
d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))
d(className="android.widget.FrameLayout").gesture((0.388, 0.591), (0.626, 0.624), (0.175, 0.533), (0.929, 0.745))
#也有预设好的,
# notes : pinch can not be set until Android 4.3.
# from edge to center. here is "In" not "in"
d(text="Settings").pinch_in(percent=100, steps=10)    #缩小图片
# from center to edge
d(text="Settings").pinch_out()    #放大图片
#fling on the specific ui object(scrollable)    快速滑动fling 
# fling.forward(default)下.vertically(default)竖直方向
d(scrollable=True).fling()
d(scrollable=True).fling.horiz.forward()    #水平方向若不能滑动,会触发点击
d(scrollable=True).fling.vert.backward()    #上
d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)
d(scrollable=True).fling.toEnd()

# scroll.forward(default).vertically(default)    一页页滑动scroll 
d(scrollable=True).scroll(steps=10)
d(scrollable=True).scroll.horiz.forward(steps=100)
d(scrollable=True).scroll.vert.backward()
d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)
d(scrollable=True).scroll.toEnd()
# scroll forward vertically until specific ui object appears
d(scrollable=True).scroll.to(text="Google")    #这里是向下滑动,底部一出现这个元素就会停止滑动
XPath

更精准的定位。在weditor可以找到每个控件的xpath

d.xpath('//*[@text="私人FM"]').get().info # 获取控件信息
# 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())
    print(el.elem) # 输出lxml解析出来的Node
    print("bounds:", el.bounds) # output tuple: (left, top, right, bottom)

   其他XPath常见用法

Session

Session represent an app lifecycle. Can be used to start app, detect app crash 当操作时app闪退,会抛出SessionBrokenError.

sess = d.session("com.example.app") # 启动应用并获取session    (应用此前未启动)
sess.close() # 停止应用
sess.restart() # 冷启动应用(重新分配新进程)
sess.running() # True or False     # check if session is ok.
'''
冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
热启动:当启动应用时,后台已有该应用的进程(例:按home键回到桌面,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
来源 https://www.zhihu.com/question/39146027/answer/337659054
'''

# When app is still running
sess(text="Music").click() # operation goes normal 正常运行 就啥也不返回
# If app crash or quit
sess(text="Music").click() # raise SessionBrokenError 闪退或者崩溃则报错

#和with一起用
with d.session("com.netease.cloudmusic") as sess:    
    sess(text="Play").click()

#Attach to the running app 附加session到已启动的应用
# launch app if not running, skip launch if already running
sess = d.session("com.netease.cloudmusic", attach=True)
# raise SessionBrokenError if not running
sess = d.session("com.netease.cloudmusic", attach=True, strict=True)
Watch 注册监控

目前采用了后台运行了一个线程的方法(依赖threading库),然后每隔一段时间dump一次hierarchy,匹配到元素之后执行相应的操作。

d.watcher.reset()    # 停止并移除所有的监控,常用于初始化

# 常用写法
d.watcher.when("取消").click()    #注册监控
d.watcher.start()    # 开始后台监控
d.watcher.start(2.0) # 默认监控间隔2.0s

d.watcher.stop()    # 停止监控

d.watcher.run()    # 强制运行所有监控

d.watcher.remove()    # 移除所有的监控

# 注册名为ANR的监控,当出现ANR和Force Close时,点击Force Close
d.watcher("ANR").when(xpath="ANR").when("Force Close").click()
d.watcher.remove("ANR")# 移除名为ANR的监控

# 其他回调例子
d.watcher.when("抢红包").press("back")
d.watcher.when("//*[@text = 'Out of memory']").call(lambda d: d.shell('am force-stop com.im.qq'))

d.xpath("继续").click() # 使用d.xpath检查元素的时候,会触发watcher(目前最多触发5次)
# 在回调中调用不会再次触发watcher
def click_callback(d: u2.Device):
    d.xpath("确定").click() # 在回调中调用不会再次触发watcher
WatchContext
ctx = d.watch_context()    #执行以后立刻开始监听
ctx.when("取消").click()    #注册
ctx.wait_stable() # 开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定)
ctx.stop()    #要用stop才能停止监听

#原作者推荐下面用法,不需要自己start和stop,见仁见智吧
#目前的这个watch_context是用threading启动的,每2s检查一次 目前还只有click这一种触发操作
with d.watch_context() as ctx:  
    ctx.when("取消").click()    #如果只有这两行,则会立即执行'取消'(如果当前页面有'取消'),没有监听的效果
    ctx.wait_stable()
    ctx.when("取消").call(lambda d: d.press("back"))    #加上下面两行就会进行3个watch check等待,这期间切换到'取消'界面,才体现监听效果

#作者内置 注册了一些监听
with d.watch_context(builtin=True) as ctx:
    # 在已有的基础上增加
    ctx.when("@tb:id/jview_view").when('//*[@content-desc="图片"]').click()
'''
            self.when("继续使用").click()
            self.when("移入管控").when("取消").click()
            self.when("^立即(下载|更新)").when("取消").click()
            self.when("同意").click()
            self.when("^(好的|确定)").click()
            self.when("继续安装").click()
            self.when("安装").click()
            self.when("Agree").click()
            self.when("ALLOW").click()
'''
Global settings 
d.HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间

# 当设备掉线时,等待设备在线时长,仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
d.WAIT_FOR_DEVICE_TIMEOUT = 70 

#其他的配置
print(d.settings)

d.settings['operation_delay'] = (.5, 1)    # 配置点击前延时0.5s,点击后延时1s

# 修改 要延迟生效的操作
# 其中 double_click, long_click 都对应click
d.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']

d.settings['xpath_debug'] = True # 开启xpath插件的调试日志
d.settings['wait_timeout'] = 20.0 # 默认控件等待时间(原生操作,xpath插件的等待时间)
Screenrecord 视频录制

这里没有使用手机中自带的screenrecord命令,是通过获取手机图片合成视频的方法,所以需要安装一些其他的依赖,如imageio, imageio-ffmpeg, numpy等 因为有些依赖比较大,推荐使用镜像安装。下面的命令可安装上述依赖。

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

有的依赖好像已经停止维护了,

ImportError: cannot import name 'create_connection' from 'websocket'
Image match 图像匹配

同上,就到这里吧

Shell commands shell命令?留坑

更多的api见

github.com/openatx/uiautomator2/README.md?plain=1#L850

Python+uiautomator2手机UI自动化测试实战 -- 2. 用法介绍_Ricky_Frog


十分钟弄懂最快的APP自动化工具uiautomator2(入门到精通) - 知乎

d.implicitly_wait(等待时长)         #单位是s

sx, ex和sy,ey分别表示起点和终点的坐标        d.swipe(sx, sy, ex, ey, 0.5)

获取屏幕尺寸:d.window_size()

滑动距离尽量大点,比如x轴起点终点可以设置系数分别为0.9, 0.1,如果你设置为0.9,0.5,否则可能会出现滑动距离太小,导致没有滑过去的情况

停止app并清理环境

一个完整的用例就写完了,当然为了演示起见,我省略了很多,比如PO模式、pytest、日志、报告等等


Performance 性能采集

原文见:uiautomator2/uiautomator2/ext /perf/

自动记录测试过程中的CPU,PSS, NET

import uiautomator2 as u2
import uiautomator2.ext.perf as perf

package_name = "com.netease.cloudmusic"
u2.plugin_register('perf', perf.Perf)


def main():
    d = u2.connect()
    d.ext_perf.package_name = package_name
    d.ext_perf.csv_output = "perf.csv" # 保存数据到perf.csv
    # d.debug = True # 采集到数据就输出,默认关闭。如果后面接d.info之类的命令,会给出更多的数据
    # d.interval = 1.0 # 数据采集间隔,默认1.0s,尽量不要小于0.5s,因为采集内存比较费时间
    d.ext_perf.start()

    # run ... tests code here ...
    d.ext_perf.stop() # 最好结束的时候调用下,虽然不调用也没多大关系
    
    # generate images from csv
    # 需要安装 matplotlib, pandas, numpy, humanize
    d.ext_perf.csv2images()


if __name__ == '__main__':
    main()

'''
csv2images函数更多的用法
d.ext_perf.csv2images("perf.csv", target_dir="./")

- PSS直接通过`dumpsys meminfo <package-name>`获取
- CPU直接读取的`/proc/`下的文件计算出来的,多核的情况,数据是有可能超过100%的
- rxBytes, txBytes 目前只有wlan的流量,tcp和udp的流量总和
- fps 通过解析`dumpsys SurfaceFlinger --list` 和 `dumpsys SurfaceFlinger --latency <VIEW>` 计算出来

PO模式:自动化测试(UI)----PO设计模式_ui自动化po模型_疯狂的大饼的博客-CSDN博客

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值