uiautomator2自动化测试
前言
最近公司想要做APP的UI自动化,安排我这边来做。这篇文章主要是作为总结和复习用。
总结
通过uiautomator2+unitest+数据库来实现app自动化,采用po模式,将app页面和业务逻辑分开,为每个页面封装一个方法,在测试用例中可以直接调用,而不涉及到具体的业务,从而提供代码的可维护性
官方教程
https://github.com/openatx/uiautomator2.git
安装
安装uiautomator2
uiautomator2是一个自动化测试开源工具,仅支持android平台的自动化测试,其封装了谷歌自带的uiautomator2测试框架,可以运行在支持Python的任一系统上
使用pip安装
pip install -U uiautomator2
对设备初始化
# init 所有的已经连接到电脑的设备
python -m uiautomator2 init
连接并启动
import uiautomator2 as u2
d = u2.connect()
# 启动app
d.app_start("com.tencent.android.qqdownloader")
# 打印设备信息
print(d.info)
安装weditor
weditor是一款基于浏览器的UI查看器,用来帮助我们查看UI元素定位。因为uiautomator是独占资源,所以当atx运行的时候uiautomatorviewer是不能用的,为了减少atx频繁的启停,就需要用到此工具
使用pip安装
pip install -U weditor
查看安装是否成功
weditor --help
运行weditor
python -m weditor
批量安装模块
创建requirements.txt文件,把所需要安装的模块都写进去,版本号可以为空,为空时自动最新版本
执行命令安装requirements.txt中的模块
pip install -r requirements.txt
已安装模块的导出
pip3 freeze >requirements.txt
使用
- usb:
d = u2.connect('10.0.0.1')
- wifi :
d = u2.connect('8c8a4d4d')
切换输入法
driver.set_fastinput_ime(False) # 关掉FastInputIME输入法,切换回系统默认输入法(此处华为手机默认输入法是华为Swype输入法)
具体实现脚本
import uiautomator2 as u2
import time
import adbutils
# 获取devices列表
devices = [d.serial for d in adbutils.adb.device_list()]
def appDown(device):
"""
下载应用
:param device:
"""
# 建立连接
d = u2.connect(device)
# 启动应用商城
d.app_start('com.tencent.android.qqdownloader')
# 设置隐式等待
d.implicitly_wait(20)
# 打印设备信息
deviceInfo = d.info
print(deviceInfo)
# 等待界面出现
inputUiObj = d.xpath('//*[@resource-id="com.tencent.android.qqdownloader:id/awt"]')
# 点击输入框
inputUiObj.click()
# 新输入框输入'淘宝'
d.xpath('//*[@resource-id="com.tencent.android.qqdownloader:id/awt"]').set_text("淘宝")
# 点击搜索
d.xpath('//*[@resource-id="com.tencent.android.qqdownloader:id/a5t"]').click()
# 安装第一个应用
d.xpath('//*[@resource-id="com.tencent.android.qqdownloader:id/a78"]/android.widget.RelativeLayout['
'1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout['
'1]').click()
time.sleep(5)
# 关闭应用
d.app_stop_all()
多线程并发执行脚本
import adbutils
import threading
from script_app.test_down import appDown
# 获取devices列表
devices = [d.serial for d in adbutils.adb.device_list()]
print(devices)
class myThread(threading.Thread):
def __init__(self, threadID, devices):
threading.Thread.__init__(self)
self.threadID = threadID
self.devices = devices
def run(self):
print("开启线程: " + self.devices)
appDown(self.devices)
threadLock = threading.Lock()
threads = []
for device in devices:
thread = myThread(1, device)
thread.start()
threads.append(thread)
# 等待所有线程完成
for t in threads:
t.join()
print("退出主线程")
APP操作
-
获取前台应用
packageName, activity
d.app_current()
-
启动应用( 默认的这种方法是先通过
atx-agent
解析apk包的mainActivity
,然后调用am start -n $package/$activity
启动)
d.app_start("com.example.app")
-
通过指定
main activity
的方式启动应用,等价于调用am start -n com.example.hello_world/.MainActivity
d.app_start("com.example.hello_world", ".MainActivity")
-
启动应用前停止此应用
d.app_start("com.example.app", stop=True)
-
停止应用, 等价于am force-stop,此方法会丢失应用数据
d.app_stop("com.example.app")
-
停止应用, 等价于
pm clear
d.app_clear('com.example.hello_world')
-
停止所有应用
d.app_stop_all()
-
停止所有应用,除了某个应用
d.app_stop_all(excludes=['com.examples.demo'])
-
得到APP图标
img = d.app_icon("com.examples.demo") img.save("icon.png") 列出所有运行中的应用 d.app_list_running()
-
确定APP是否启动,也可以通过Session来判断
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(默认) // or d.wait_activity(".ApiDemos", timeout=10) # default timeout 10.0 seconds
截图与hierarchy提取
截图
# take screenshot and save to a file on the computer, require Android>=4.2.
d.screenshot("home1.jpg")
# get PIL.Image formatted images. Naturally, you need pillow installed first
image = d.screenshot() # default format="pillow"
image.save("home2.jpg") # or home.png. Currently, only png and jpg are supported
# get raw jpeg data
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)
获取页面元素
# get the UI hierarchy dump content (unicoded).
xml = d.dump_hierarchy()
查找(定位)元素
用driver(propertyParameter)去查找元素
# 获取元素
inputUiObj = d(resourceId="com.liinji.billingas.test:id/userName", className="android.widget.EditText")
# 返回的元素类型 -<class 'uiautomator2.session.UiObject'>
print("type(inputUiObj)=%s" % type(inputUiObj))
# 想要继续获取元素属性,则可以通过info
inputUiObjInfo = inputUiObj.info
# info类型为 -<class 'dict'>
print("type(inputUiObjInfo)=%s" % type(inputUiObjInfo))
print(inputUiObjInfo)
# 获取元素属性值
userName = inputUiObjInfo["text"] # 请输入用户名
print(userName)
用driver.xpath(xpathSelector)去查找元素
# 获取元素
inputUiObj = d.xpath('//*[@resource-id="com.liinji.billingas.test:id/userName"]')
# 返回元素类型 -<class 'uiautomator2.xpath.XPathSelector'>
print("type(inputUiObj)=%s" % type(inputUiObj))
# 想要继续获取元素属性,则可以通过get
inputUiObjInfo = inputUiObj.get()
# get类型为 -<class 'uiautomator2.xpath.XMLElement'>
print("type(inputUiObjInfo)=%s" % type(inputUiObjInfo))
print(inputUiObjInfo)
# 然后才能去用inputXpathElem.attrib获取属性值
# xxx.attrib数据类型 -<class 'lxml.etree._Attrib'>
print("type(inputUiObjInfo.attrib)=%s" % type(inputUiObjInfo.attrib))
# 获取属性值
inputUserName = inputUiObjInfo.attrib['text'] # 请输入用户名
print(inputUserName)
模拟触控操作
通过元素文本属性来点击
# 点击通过元素文本,默认点击元素文本中心
d(text='请输入用户名').click(offset=(1, 1)) # 点击文本右下角
# 如果元素存在,就点击,超时时间默认0s
d(text='验证码登录').click_exists(timeout=10)
滑动操作
# 滑动界面
# 自带封装了swipe(),参数up,down,left,right分别代表上下左右滑动
d(text="Settings").swipe("right")
d(text="Settings").swipe("left", steps=10)
d(text="Settings").swipe("up", steps=20) # 1 steps is about 5ms, so 20 steps is about 0.1s
d(text="Settings").swipe("down", steps=20)
# steps参数:控制滑动时间
d.xpath('//*[@resource-id="com.liinji.billingas.test:id/userName"]').swipe('up')
d(text='验证码登录').swipe("up", steps=20) # 1 steps is about 5ms, so 20 steps is about 0.1s
# 整个屏幕滑动
d.swipe_ext('right')
# 屏幕右滑,滑动距离为屏幕宽度的90%
d.swipe_ext("right", scale=0.9)
从一个坐标拖拽到另一个坐标
# 从一个坐标拖拽到另一个坐标
d.drag(10, 10, 20, 20)
d.drag(10, 10, 20, 20, duration=0.5)
模拟按下后的连续操作,如九宫格解锁
# 模拟按下后的连续操作,如九宫格解锁
d.touch.down(10, 10) # 模拟按下
time.sleep(.01) # down 和 move 之间的延迟,自己控制
d.touch.move(15, 15) # 模拟移动
d.touch.up() # 模拟抬起
模拟两指缩放操作
# 缩小
d(text='验证码登录').pinch_in(percent=100, steps=50)
# 放大
d(text='验证码登录').pinch_out(percent=100, steps=50)
硬按键操作
用于模拟用户对手机硬按键或系统按键的操作
模拟按 Home 或 Back 键
d.press("back")
d.press("home")
解锁屏幕
d.unlock()
模拟输入,需要光标已经在输入框中才可以
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") # 模拟输入法的搜索
执行ADB shell命令
直接通过Python来执行ADB shell中的指令,并得到反馈。
执行shell命令,获取输出和exitCode
# 执行shell命令,获取输出和exitCode
output, exit_code = d.shell("ps -A", timeout=60)
# 仅得到输出
output = d.shell("pwd").output
# 仅得到Exitcode
exit_code = d.shell("pwd").exit_code
推送文件到ADB设备中
# 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)
获取文件到本地
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")
元素操作或Selector
这是Uiautomator2最为关键的核心功能,测试者可以根据界面中的元素来判断当前画面是否符合预期或基于界面元素进行点按滑动等操作
多属性组合,实现元素定位
# Select the object with text 'Clock' and its className is 'android.widget.TextView'
d(text='Clock', className='android.widget.TextView').click()
通过子节的和兄弟节点,实现定位
# children
# get the children or grandchildren
d(className="android.widget.ListView").child(text="Bluetooth")
# siblings
# get siblings
d(text="Google").sibling(className="android.widget.ImageView")
根据子节点的Text或 Description或Instance来定位元素
特别提下下面代码中的这个allow_scroll_search功能,它调用UT2自动滚动直到找到对应元素
根据子节点的Text定位
# get the child matching the condition className="android.widget.LinearLayout"
# and also its children or grandchildren with text "Bluetooth"
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text("Bluetooth", className="android.widget.LinearLayout")
allow_scroll_searc参数实现自动滚动,直到找到对应元素
# get children by allowing scroll search
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text(
"Bluetooth",
allow_scroll_search=True,
className="android.widget.LinearLayout"
)
等待某个元素出现
# advanced usage
d(text="Settings").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)
d.xpath("立即开户").wait() # 等待元素,最长等10s(默认)
d.xpath("立即开户").wait(timeout=10) # 修改默认等待时间
输入框的操作
d(text="Settings").get_text() # get widget text
d(text="Settings").set_text("My text...") # set the text
d(text="Settings").clear_text() # clear the text
等待某个元素出现或消失
# wait until the ui object appears
d(text="Settings").wait(timeout=3.0) # return bool
# wait until the ui object gone
d(text="Settings").wait_gone(timeout=1.0)
守护
# 常用写法,注册匿名监控
d.watcher.when("安装").click()
# 注册名为ANR的监控,当出现ANR和Force Close时,点击Force Close
d.watcher("ANR").when(xpath="ANR").when("Force Close").click()
# 其他回调例子
d.watcher.when("抢红包").press("back")
d.watcher.when("//*[@text = 'Out of memory']").call(lambda d: d.shell('am force-stop com.im.qq'))
# 移除ANR的监控
d.watcher.remove("ANR")
# 移除所有的监控
d.watcher.remove()
# 开始后台监控
d.watcher.start()
d.watcher.start(2.0) # 默认监控间隔2.0s
# 强制运行所有监控
d.watcher.run()
# 停止监控
d.watcher.stop()
# 停止并移除所有的监控,常用于初始化
d.watcher.reset()