干货一文测试工具(元素操作设备测试方法)「元素测试仪器」

霍格沃兹测试学院是 python-uiautomator2 金牌赞助商,跟着开源项目作者学测试开发实战,文末加群
一、背景简介Google 官方提供了一个 Android 自动化测试工具(Java 库),基于 Accessibility 服务,功能很强,可以对第三方 App 进行测试,获取屏幕上任意一个 App 的任意一个控件属性,并对其进行任意操作,但有两个缺点:测试脚本只能使用 Java 语言;测试脚本要打包成 jar 或者 apk 包上传到设备上才能运行;实际工作中,我们希望测试逻辑能够用 Python 编写,能够在电脑上运行的时候就控制手机
所以基于这个目的开发了 python-uiautomator2 自动化测试开源工具,其封装了谷歌自带的 uiautomator2 测试框架,可以运行在支持 Python 的任一系统上,目前版本为 V2.10.2
GitHub 开源地址:https://github.com/openatx/uiautomator2二、工作原理如图所示,python-uiautomator2 主要分为两个部分,python 客户端,移动设备python 端: 运行脚本,并向移动设备发送 HTTP 请求;移动设备:移动设备上运行了封装了 uiautomator2 的 HTTP 服务,解析收到的请求,并转化成 uiautomator2 的代码;整个过程:在移动设备上安装 atx-agent(守护进程),随后 atx-agent 启动 uiautomator2 服务(默认 7912 端口)进行监听;在 PC 上编写测试脚本并执行(相当于发送 HTTP 请求到移动设备的 server 端);移动设备通过 WIFI 或 USB 接收到 PC 上发来的 HTTP 请求,执行制定的操作;三、安装与启动3.1 安装 uiautomator2使用 pip 安装pipinstall-Uuiautomator2安装完成后,使用如下 python 代码查看环境是事配置成功说明:后文中所有代码都需要导入 uiautomator2 库,为了简化我使用 u2 代替,d 代表 driverimportuiautomator2asu2#连接并启动d=u2.connect()print(d.info)能正确打印出设备的信息则表示安装成功注意:需要安装 adb 工具,并配置到系统环境变量,才能操作手机
安装有问题可以到 issue 列表查询:https://github.com/openatx/uiautomator2/wiki/Common-issues3.2 安装 weditorweditor 是一款基于浏览器的 UI 查看器,用来帮助我们查看 UI 元素定位
因为 uiautomator 是独占资源,所以当 atx 运行的时候 uiautomatorviewer 是不能用的,为了减少 atx 频繁的启停,就需要用到此工具使用 pip 安装pipinstall-Uweditor查看安装是否成功weditor--help出现如下信息表示安装成功运行 weditorpython-mweditor#或者直接在命令行运行weditor四、元素定位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 查看到名称描述texttext 是指定文本的元素textContainstext 中包含有指定文本的元素textMatchestext 符合指定正则的元素textStartsWithtext 以指定文本开头的元素classNameclassName 是指定类名的元素classNameMatchesclassName 类名符合指定正则的元素descriptiondescription 是指定文本的元素descriptionContainsdescription 中包含有指定文本的元素descriptionMatchesdescription 符合指定正则的元素descriptionStartsWithdescription 以指定文本开头的元素checkable可检查的元素,参数为 True,Falsechecked已选中的元素,通常用于复选框,参数为 True,Falseclickable可点击的元素,参数为 True,FalselongClickable可长按的元素,参数为 True,Falsescrollable可滚动的元素,参数为 True,Falseenabled已激活的元素,参数为 True,Falsefocusable可聚焦的元素,参数为 True,Falsefocused获得了焦点的元素,参数为 True,Falseselected当前选中的元素,参数为 True,FalsepackageNamepackageName 为指定包名的元素packageNameMatchespackageName 为符合正则的元素resourceIdresourceId 为指定内容的元素resourceIdMatchesresourceId 为符合指定正则的元素4.3 子元素和兄弟定位子元素定位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左边的Bd(A).right(B),#选择A右边的Bd(A).up(B),#选择A上边的Bd(A).down(B),#选择A下边的B#选择WIFI右边的开关按钮d(text='Wi‑Fi').right(resourceId='android:id/widget_frame')4.5 元素常用 API表格标注有 @property 装饰的类属性方法,均为下方示例方式d(test="Settings").exists方法描述返回值备注exists()判断元素是否存在True,Flase@propertyinfo()返回元素的所有信息字典@propertyget_text()返回元素文本字符串set_text(text)设置元素文本Noneclear_text()清空元素文本Nonecenter()返回元素的中心点位置(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}可以通过上方信息分别获取元素的所有属性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.1 单击d(text='Settings').click()#单击直到元素消失,超时时间10,点击间隔1d(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拖动至点2d.drag(x1,y1,x2,y2)5.4 滑动滑动有两个,一个是在 driver 上操作,一个是在元素上操作元素上操作从元素的中心向元素边缘滑动#在 Setings 上向上滑动
steps 默认为 10#1步约为5毫秒,因此20步约为0.1sd(text="Settings").swipe("up",steps=20)
driver 上操作即对整个屏幕操作#实现下滑操作x,y=d.window_size()x1=x/2y1=y0.1y2=y0.9d.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)#等待元素消失,返回TrueFalse,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")Takescreenshotofwidgetim=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 支持的按键如下"""presskeyvianameorkeycode.Supportedkeynameincludes:home,back,left,right,up,down,center,menu,search,enter,delete(ordel),recent(recentapps),volume_up,volume_down,volume_mute,camera,power."""keyevent 是通过 “adb shell input keyevent” 方式输入,支持按键更加丰富更多详细的按键信息 https://developer.android.com/reference/android/view/KeyEvent.html5.8.3 输入法切换#切换成ui2的输入法,这里会隐藏掉系统原本的输入法,默认是使用系统输入法#当传入False时会使用系统默认输入法,默认为Fasled.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='notoast')#清空toast缓存d.toast.reset()5.10 监控界面使用 wather 进行界面的监控,可以用来实现跳过测试过程中的弹框当启动 wather 时,会新建一个线程进行监控可以添加多个 watcher用法#注册监控,当界面内出现有allow字样时,点击allowd.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()#监控ALLOWwct.when("ALLOW").click()#监控OKwct.when('OK').click()#开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定)wct.wait_stable()#其它实现代码#停止监控wct.stop()5.11 多点滑动这里可以用来实现图案解锁使用 touch 类#模拟按下不放手touch.down(x,y)#停住3Stouch.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.1 截图d.screenshot('test.png')6.2 录制视频这个感觉是比较有用的一个功能,可以在测试用例开始时录制,结束时停止录制,然后如果测试 fail
则上传到测试报告,完美复原操作现场,具体原理后面再去研究
首先需要下载依赖,官方推荐使用镜像下载:pip3install-U"uiautomator2[image]"-ihttps://pypi.doubanio.com/simple执行录制:#启动录制,默认帧率为20d.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.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)#源码说明defapp_start(self,package_name:str,activity:Optional[str]=None,wait:bool=False,stop:bool=False,use_monkey:bool=False):"""LaunchapplicationArgs:package_name(str):packagenameactivity(str):appactivitystop(bool):Stopappbeforestartingtheactivity.(requireactivity)use_monkey(bool):usemonkeycommandtostartappwhenactivityisnotgivenwait(bool):waituntilappstarted.defaultFalse"""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,表示只要后台有这个应用的进程就会返回PIDd.app_wait('com.xueqiu.android',60,front=True)7.8 卸载应用#卸载成功返回true,没有此包或者卸载失败返回Falsed.app_uninstall('com.xueqiu.android')#卸载所有自己安装的第三方应用,返回卸载app的包名列表#excludes表示不卸载的列表#verbose为true则会打印卸载信息d.app_uninstall_all(excludes=[],verbose=True)卸载全部应用返回的包名列表并一定是卸载成功了,最好使用 verbose=true 打印一下信息,这样可以查看到是否卸载成功uninstallingcom.xueqiu.androidOKuninstallingcom.android.cts.verifierFAIL或者可以修改一下源码,使其只输出成功的包名,注释的为增加的代码,未注释的是源码defapp_uninstall_all(self,excludes=[],verbose=False):"""Uninstallallapps"""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=[]forpkg_nameinpkgs:ifverbose:print("uninstalling",pkg_name,"",end="",flush=True)ok=self.app_uninstall(pkg_name)ifverbose:print("OK"ifokelse"FAIL")#增加如下语句,当成功则将包名加入list#ifok:#sucess_list.append(pkg_name)#返回成功的列表#returnsucess_listreturnpkgs八、其它实用方法8.1 连接设备#当PC只连接了一个设备时,可以使用此种方式d=u2.connect()#返回的是Device类,此类继承方式如下classDevice(_Device,_AppMixIn,_PluginMixIn,_InputMethodMixIn,_DeprecatedMixIn):"""Deviceobject"""#forcompatiblewitholdcodeSession=Deviceconnect() 可以使用如下其它方式进行连接#当PC与设备在同一网段时,可以使用IP地址和端口号通过WIFI连接,无需连接USB线connect("10.0.0.1:7912")connect("10.0.0.1")#usedefault7912portconnect("http://10.0.0.1")connect("http://10.0.0.1:7912")#多个设备时,使用设备号指定哪一个设备connect("cff1123ea")#adbdeviceserialnumber8.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":"freedomturboXL","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":"6GB"},"cpu":{"cores":8,"hardware":"QualcommTechnologies,IncSDM665"},"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()#例分辨率为10801920#手机竖屏状态返回(1080,1920)#横屏状态返回(1920,1080)8.2.4 获取 IP 地址#返回ip地址字符串,如果没有则返回Noned.wlan_ip8.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.5Sd.settings['operation_delay']=(2,4.5)#修改延迟生效方法d.settings['operation_delay_methods']={'click','press','send_keys'}#修改默认等待d.settings['wait_timeout']=108.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=Trued.info#输出15:52:04.736$curl-XPOST-d'{"jsonrpc":"2.0","id":"0eed6e063989e5844feba578399e6ff8","method":"deviceInfo","params":{}}''http://localhost:51046/jsonrpc/0'15:52:04.816Response(79ms)>>>{"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)8.4 亮灭屏#亮屏d.screen_on()#灭屏d.screen_off()8.5 屏幕方向#设置屏幕方向d.set_orientation(value)#获取当前屏幕方向d.orientationvalue 值参考,只要是元组中的任一一个值就可以
#正常竖屏(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,异常为1output,exit_code=d.shell(["ls","-l"],timeout=60)8.8.2 执行阻塞命令(持续执行的命令)#返回一个命令的数据流output为requests.models.Responseoutput=d.shell('logcat',stream=True)try:#按行读取,iter_lines为迭代响应数据,一次一行forlineinoutput.iter_lines():print(line.decode('utf8'))finally:output.close()源码描述defshell(self,cmdargs:Union[str,List[str]],stream=False,timeout=60):"""Runadbshellcommandwithargumentsandreturnitsoutput.Requireatx-agent>=0.3.3Args:cmdargs:strorlist,example:"ls-l"or["ls","-l"]timeout:secondsofcommandrun,worksonwhenstreamisFalsestream:boolusedforlongrunningprocess.Returns:(output,exit_code)whenstreamisFalserequests.ResponsewhenstreamisTrue,youhavetocloseitafterusingRaises:RuntimeErrorForatx-agentisnotsupportreturnexitcodenow.Whencommandgotsomethingwrong,exit_codeisalways1,otherwiseexit_codeisalways0"""8.9 session(目前已经被弃用)8.10 停止 UI2 服务因为有 atx-agent 的存在,Uiautomator 会被一直守护着,如果退出了就会被重新启动起来
但是 Uiautomator 又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的 uiautomatorviewer 就都不能用了,除非关掉该框架本身的 uiautomator使用代码停止d.service("uiautomator").stop()手动停止直接打开 ATX APP(init 成功后,就会安装上),点击关闭 UIAutomator以上,欢迎大家一起交流探讨
干货一文测试工具(元素操作设备测试方法)
(图片来源网络,侵删)

联系我们

在线咨询:点击这里给我发消息