添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

[TOC]

uiautomator2是一个自动化测试开源工具,仅支持android平台的自动化测试,其封装了谷歌自带的uiautomator2测试框架,可以运行在支持Python的任一系统上,目前版本为2.10.2

开源库地址:

https://github.com/openatx/uiautomator2

2.工作原理

引用自https://testerhome.com/topics/11357

如图所示,python-uiautomator2主要分为两个部分,python客户端,移动设备

  • python端: 运行脚本,并向移动设备发送HTTP请求
  • 移动设备:移动设备上运行了封装了uiautomator2的HTTP服务,解析收到的请求,并转化成uiautomator2的代码。
  • 在移动设备上安装atx-agent(守护进程), 随后atx-agent启动uiautomator2服务(默认7912端口)进行监听
  • 在PC上编写测试脚本并执行(相当于发送HTTP请求到移动设备的server端)
  • 移动设备通过WIFI或USB接收到PC上发来的HTTP请求,执行制定的操作
  • 3.安装与启动

    3.1 安装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这里查看一下有没有相同的问题

    3.2 安装weditor

    weditor是一款基于浏览器的UI查看器,用来帮助我们查看UI元素定位。

    因为uiautomator是独占资源,所以当atx运行的时候uiautomatorviewer是不能用的,为了减少atx频繁的启停,就需要用到此工具

    使用pip安装

    pip install -U weditor
    

    查看安装是否成功

    weditor --help
    

    出现如下信息表示安装成功

    运行weditor

    python -m weditor
    #或者直接在命令行运行
    weditor
    

    4. 元素定位

    4.1 使用方法

    d(定位方式=定位值)
    element = d(text='Phone')
    #这里返回的是一个列表,当没找到元素时,不会报错,只会返回一个长度为0的列表
    #当找到多个元素时,会返回多个元素的列表,需要加下标再定位
    element[0].click()
    #获取元素个数
    print(element.count)
    

    4.2 支持的定位方式

    ui2支持 android 中 UiSelector 类中的所有定位方式,详细可以在这个网址查看https://developer.android.com/reference/android/support/test/uiautomator/UiSelector

    整体内容如下,所有的属性可以通过weditor查看到

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

    4.4 相对定位

    相对定位支持在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')
    

    4.5 元素常用API

    表格标注有@property装饰的类属性方法,均为下方示例方式

    d(test="Settings").exists
      "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
    

    可以通过上方信息分别获取元素的所有属性

    4.6 XPATH定位

    因为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()
    

    5. 设备交互

    5.1 单击

    d(text='Settings').click()
    #单击直到元素消失,超时时间10,点击间隔1
    d(text='Settings').click_gone(maxretry=10, interval=1.0)
    

    5.2 长按

    d(text='Settings').long_click()
    

    5.3 拖动

    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)
    

    5.4 滑动

    滑动有两个,一个是在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")
    

    5.5 双指操作

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

    5.6 等待元素出现或者消失

    # 等待元素出现
    d(text="Settings").wait(timeout=3.0)
    # 等待元素消失,返回True False,timout默认为全局设置的等待时间
    d(text='Settings').wait_gone(timeout=20)
    

    5.7 滚动界面

    设置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")
    
    Take screenshot of widget
    im = d(text="Settings").screenshot()
    im.save("settings.jpg")
    

    5.8 输入

    5.8.1 输入自定义文本

    # 使用adb广播的方式输入
    d.send_keys('hello')
    # 清空输入框
    d.clear_text()
    

    5.8.2 输入按键

    # 发送回车
    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

    5.8.3 输入法切换

    # 切换成ui2的输入法,这里会隐藏掉系统原本的输入法,默认是使用系统输入法
    # 当传入False时会使用系统默认输入法,默认为Fasle
    d.set_fastinput_ime(True)
    # 查看当前输入法
    d.current_ime()
    ('com.github.uiautomator/.FastInputIME', True)
    

    5.8.4 模拟输入法功能

    可以模拟的功能有 go ,search ,send ,next, done ,previous

    如果使用press输入按键无效,可以尝试使用此方法输入

    # 搜索功能
    d.send_action("search")
    

    5.9 toast操作

    # 获取toast,当没有找到toast消息时,返回default内容
    d.toast.get_message(timout=5,default='no toast')
    # 清空toast缓存
    d.toast.reset()
    

    5.9 监控界面

    使用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()
    

    2.11.0版本新增了一个watch_context方法,写法相比watcher更简洁,官方推荐使用此方法来实现监控
    目前只支持click()这一种方法

    wct = d.watch_context() # 监控 ALLOW wct.when("ALLOW").click() # 监控OK wct.when('OK').click() # 开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定) wct.wait_stable() #其它实现代码 # 停止监控 wct.stop()

    5.10 多点滑动

    这里可以用来实现图案解锁

    使用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)
    

    6. 图像操作

    6.1 截图

    d.screenshot('test.png')
    

    6.2 录制视频

    这个感觉是比较有用的一个功能,可以在测试用例开始时录制,结束时停止录制,然后如果测试fail。则上传到测试报告,完美复原操作现场,具体原理后面再去研究

    首先需要下载依赖,官方推荐使用镜像下载

    pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
    
    # 启动录制,默认帧率为20
    d.screenrecord('test.mp4')
    # 其它操作
    time.sleep(10)
    #停止录制,只有停止录制了才能看到视频 
    d.screenrecord.stop()
    

    6.3 图片识别点击

    下载与录制视频同一套依赖

    这个功能是首先手动截取需要点击目标的图片,然后ui2在界面中去匹配这个图片,目前我尝试了精确试不是很高,误点率非常高,不建议使用

    d.image.click('test.png') # 匹配图片,返回相似度和坐标 # {'similarity': 0.9314796328544617, 'point': [99, 630]} d.image.match('test.png')

    7. 应用管理

    7.1 获取当前界面的APP信息

    d.app_current()
    #返回当前界面的包名,activity及pid
        "package": "com.xueqiu.android",
        "activity": ".common.MainActivity",
        "pid": 23007
    

    7.2 安装应用

    可以从本地路径及url下载安装APP,此方法无返回值,当安装失败时,会抛出RuntimeError异常

    # 本地路径安装 
    d.app_install('test.apk')
    # url安装 
    d.app_install('http://s.toutiao.com/UsMYE/')
    

    7.3 运行应用

    默认当应用在运行状态执行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
    

    7.4 停止应用

    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'])
    

    7.5 获取应用信息

    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
    

    7.6 获取应用图标

    img = d.app_icon('com.xueqiu.android')
    img.save('icon.png')
    

    7.7 等待应用启动

    # 等待此应用变为当前应用,返回pid,超时未启动成功则返回0
    # front为true表示等待app成为当前app,
    # 默认为false,表示只要后台有这个应用的进程就会返回PID
    d.app_wait('com.xueqiu.android',60,front=True)
    

    7.8 卸载应用

    # 卸载成功返回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
    

    8. 其它实用方法

    8.1 连接设备

    #当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
    

    8.2 获取设备及driver信息

    8.2.1 获取driver信息

    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
    

    8.2.2 获取设备信息

    会输出测试设备的所有信息,包括电池,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
    

    8.2.3 获取屏幕分辨率

    # 返回(宽,高)元组
    d.window_size()
    # 例 分辨率为1080*1920
    # 手机竖屏状态返回 (1080,1920)
    # 横屏状态返回 (1920,1080)
    

    8.2.4 获取 IP 地址

    # 返回ip地址字符串,如果没有则返回None
    d.wlan_ip
    

    8.3 driver全局设置

    8.3.1 使用settings设置

    查看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
    

    8.3.2 使用方法或者属性设置

  • http默认请求超时时间
  • # 默认值60s, 
    d.HTTP_TIMEOUT = 60 
    
  • 当设备掉线时,等待设备在线时长
  • # 仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
    d.WAIT_FOR_DEVICE_TIMEOUT = 70 
    
  • 元素查找默认等待时间
  • # 打不到元素时,等待10后再报异常
    d.implicitly_wait(10.0)
    
  • 打开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}}
    
    # 相当于 time.sleep(10)
    d.sleep(10)
    

    8.4 亮灭屏

    d.screen_on() d.screen_off()

    8.5 屏幕方向

    # 设置屏幕方向
    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))
    

    8.6 打开通知栏与快速设置

    打开通知栏

    d.open_notification()
    

    打开快速设置

    d.open_quick_settings()
    

    8.7 文件导入导出

    8.7.1 导入文件

    # 如果是目录,这里"/sdcrad/"最后一个斜杠一定要加,否则会报错
    d.push("test.txt","/sdcrad/")
    d.push("test.txt","/sdcrad/test.txt")
    

    8.7.2 导出文件

    d.pull('/sdcard/test.txt','text.txt')
    

    8.8 执行shell命令

    使用shell方法执行

    8.8.1 执行非阻塞命令

    output返回的是一个整体的字符串,如果需要抽取值,需要对output进行解析提取处理

    # 返回输出和退出码,正常为0,异常为1
    output,exit_code = d.shell(["ls","-l"],timeout=60)
    

    8.8.2 执行阻塞命令(持续执行的命令)

    # 返回一个命令的数据流 output为requests.models.Response
    output = d.shell('logcat',stream=True)
        # 按行读取,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
    

    8.9 session(目前已经被弃用)

    8.10 停止UI2服务

    因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator

    使用代码停止

    d.service("uiautomator").stop()
    

    直接打开ATX APP(init成功后,就会安装上),点击关闭UIAutomator