uiautomator2 是一个可以使用Python对Android设备进行UI自动化的库。其底层基于Google uiautomator,Google提供的uiautomator库可以获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作
安装JDK,请参考此文章
安装Android SDK,构建工具版本需大于24,下载并安装工具包时请注意版本,SDK配置请参考此文章
安装uiautomator2
pip install uiautomator2 # 安装uiautomator2 uiautomator2 version # 查看版本 uiautomator2 --help # 查看帮助
安装查看器,用于元素定位辅助
pip install weditor # 安装weditor weditor -v # 查看版本 weditor --help # 查看帮助
启动查看器,在命令行输入weditor
,或者创建weditor桌面快捷键weditor --shortcut
,通过桌面图标运行程序
首次连接设备会安装【ATX】和【com.github.uiautomator.test】两个软件
手机开启【开发者模式】,打开【USB调试模式】数据线连接手机,选择【传输文件】
通过cmd进入命令行页面,通过adb devices
查看设备是否连接成功,adb相关命令请查看此文章
设备连接成功后打开查看器,在查看器页面点击【Connect】,出现绿色树叶表示连接成功,左侧会出现手机投屏(手机亮屏状态)
通过Python脚本连接手机:
import uiautomator2 as u2 # 导入uiautomator2库并重命名为u2 driver = u2.connect() # 连接手机,若电脑只连接了一部手机,则不需要设备信息 print(driver.info) # 打印设备信息
输入如下系统信息,说明连接手机成功,就可以开始使用uiautomator2库啦
{'currentPackageName': 'com.oppo.launcher', 'displayHeight': 2297, 'displayRotation': 0, 'displaySizeDpX': 360, 'displaySizeDpY': 800, 'displayWidth': 1080, 'productName': 'OnePlus9R_CH', 'screenOn': True, 'sdkInt': 30, 'naturalOrientation': True}
driver = u2.connect("48fd6742") # 若电脑连接了多部手机,则需要添加设备序列号
driver = u2.connect("48fd6742") # 连接设备 driver.screen_on() # 点亮屏幕 driver.screen_off() # 熄屏 driver.unlock() # 解锁,真机测试发现密码输入页面无法定位,部分APP也有无法同步登录页面导致不能定位的情况 print(driver.app_info("com.sankuai.meituan")) # 获取指定APP的信息 print(driver.app_list_running())# 列出所有正在运行的APP print(driver.app_current()) # 获取当前打开的APP信息 print(driver.window_size()) # 获取屏幕大小 print(driver.device_info) # 获取详细的设备信息 print(driver.serial) # 获取设备序列号 print(driver.wlan_ip) # 获取设备IP地址 driver.press("back") # 点击返回键 driver.open_notification() # 打开通知栏消息页,关闭通知栏用滑屏操作 driver.open_quick_settings() # 打开通知栏中的快速设置 driver.info.get("screen_on") # 获取当前屏幕是否为亮起状态 driver.swipe(552,2066,552,700) # 按绝对坐标滑动屏幕,默认滑动时长0.5s driver.swipe_ext("up",scale=0.5)# 按方向滑动屏幕,设置滑动距离为屏幕宽度的50%,默认是90% driver.open_url("https://www.baidu.com") # 直接调用默认浏览器并访问指定网站 driver(description="信息").click() # 单击短信APP driver.double_click(0.375, 0.496) # 双击指定的相对坐标 driver(description="信息").long_click(1) # 长按短信APP,默认长按0.5s driver(description="短信").send_keys("12")# 定位元素并输入文本 driver(description="短信").set_text("A12")# 也是定位元素并输入文本 driver(description="短信").clear_text() # 清空输入的信息 driver(resourceId="com.sankuai.meituan:id/passport_mobile_phone").get_text() # 获取文本内容 driver(text="美团").drag_to(0.375, 0.375,duration=1) # 将美团APP拖动到指定地点,持续时间1s,默认0.5s driver.screenshot(r"D:\Download\test.png") # 保存截屏到指定位置 driver.push(r"C:\Download\test.png","/sdcard/") # 将截图推送到手机中 driver.pull("/sdcard/rider.txt","rider.txt") # 将手机中的文件传送到电脑 driver.app_icon("com.sankuai.meituan").save(r"D:\Download\icon.png") # 获取APP图标并保存到指定位置 driver.app_start("com.sankuai.meituan",stop=True) # 指定包名启动APP,启动前先结束应用运行状态 driver.implicitly_wait(10) # 原生操作,隐式等待,全局有效 driver(description="天天领红包").exists() # 判断元素是否出现,真返回True,否返回False driver(description="电影/演出").wait(timeout=5) # 等待元素出现,超时时间为5s driver(description="跑腿").wait_gone(timeout=5) # 等待元素消失,超时时间为5s driver.app_wait("com.sankuai.meituan",timeout=30,front=True) # 等待程序开始前台运行,默认超时时间20s driver.wait_activity("com.meituan.mmp.lib.mp.MPActivity0",timeout=5) # 等待活动页加载完成,默认超时时间10s driver.app_install(r"D:\Download\meituan.apk") # 使用本地安装包安装 driver.app_install("http://www.meituan.com/mobile/download/meituan/android/meituan?from=new") # 在线下载安装 driver.app_uninstall("com.sankuai.meituan") # 卸载APP driver.app_stop("com.sankuai.meituan") # 关闭APP
所选元素的属性都可以用来定位,目的是通过这些属性获取唯一的定位元素,属性可以组合定位,常用的定位如下
driver(text="运动健康") # 通过文本定位 driver(description="我的") # 通过描述定位 driver.xpath('//*[@text="今日特价"]') # 通过Xpath定位 driver(className="android.widget.ImageView") # 通过className定位 driver(resourceId="com.sankuai.meituan:id/button") # 通过ID定位 driver(resourceId="com.android.systemui:id/tile_label", text="省电模式") # 通过组合定位
还可以根据层次结构中的节点进行定位,如下
# 在android.widget.GridLayout的兄弟节点中找className=android.view.View的元素 driver(className="android.widget.GridLayout").sibling(className="android.view.View") # 在android.widget.GridLayout的子节点中找第4个className=android.view.View的元素 driver(className="android.widget.GridLayout").child_by_instance(3,className="android.view.View") # 在android.widget.LinearLayout的子节点中找className=android.widget.TextView且文本为“骑车”的元素,allow_scroll_search=True表示允许滑动屏幕进行查找 driver(className="android.widget.LinearLayout")\ .child_by_text("骑车",allow_scroll_search=True,className="android.widget.TextView") # 在android.widget.FrameLayout的子节点中找className=android.view.ViewGroup且描述为“评论插图”的元素 driver(className="android.widget.FrameLayout")\ .child_by_description("评论插图",allow_scroll_search=True,className="android.view.ViewGroup") # 通过方向(up/down/left/right)定位元素,如下:定位并点击网易云音乐APP右边的APP driver(text="网易云音乐").right(className="android.widget.TextView").click()
使用assert判断实际结果是否与预期结果一致
# 获取登录失败的提示信息 text = driver(resourceId="com.sankuai.meituan:id/passport_account_tips").get_text() # 判断提示是否正确 assert text == "账号或密码错误,请重新输入" # 因登录成功后才会出现成长值,所以通过查看成长值的元素是否存在来判断是否登录成功 assert driver(resourceId="com.sankuai.meituan:id/grouth_tv").exists
对于没有焦点的,显示时间有限的提示框,使用toast
进行断言
tips = driver.toast.get_message() # 获取错误提示信息 assert "用户名或密码错误" in tips # 判断获取的信息中是否有"用户名或密码错误"
示例一:设置图形验证码并解锁,真机未能实现,无法进入解锁界面,可能是权限问题,用模拟器执行成功
import time import uiautomator2 as u2 driver = u2.connect() # 连接设备 driver(text="设置").click() # 点击设置APP # ↓使用节点方式定位元素 driver(className="android.widget.LinearLayout").child_by_text("安全", allow_scroll_search=True,className="android.widget.TextView").click() driver(resourceId="android:id/title", text="屏幕锁定").click() # 使用ID方式定位元素 # ↓使用xpath方式定位元素 driver.xpath('//*[@resource-id="com.android.settings:id/list"]/android.widget.LinearLayout[3]/android.widget.RelativeLayout[1]').click() driver.swipe_points([(0.223, 0.658), (0.5, 0.832), (0.5, 0.652), (0.78, 0.487)], 0.2) # 多点滑动,设置图形密码 driver(resourceId="com.android.settings:id/footerRightButton").click() # 点击【继续】 driver.swipe_points([(0.223, 0.658), (0.5, 0.832), (0.5, 0.652), (0.78, 0.487)], 0.2) driver(resourceId="com.android.settings:id/footerRightButton").click() # 点击【确定】 driver(resourceId="com.android.settings:id/redaction_done_button").click() # 点击【完成】 driver.press("power") # 点击电源键 time.sleep(3) # 操作太快只会熄灭点亮屏幕,所以等待3秒,让设备上锁 driver.unlock() # 解锁操作 driver.swipe_points([(0.273, 0.728), (0.5, 0.867), (0.5, 0.728), (0.719, 0.591)], 0.2) # 绘制图形密码 assert driver(text="安全").exists # 判断是否返回到安全页面
示例二:以美团APP为例,测试登录、查看订单、搜索商品相关操作,因存在大量定位元素操作,不使用框架就不便于维护,所以使用Pytest+Allure及PO模型实现此操作,结构如下图所示:
以登录为例,首先创建基类basepage.py
文件,封装一些公共方法,比如:定位、点击、输入、清除、获取文本信息、断言等
import re class BasePage(): # 构造函数 def __init__(self, driver): self.driver = driver def click(self, element): # 点击 if str(element).startswith("com"): # 若开头是com则使用ID定位 self.driver(resourceId=element).click() # 点击定位元素 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 self.driver.xpath(element).click() # 点击定位元素 else: # 若以上两种情况都不是,则使用描述定位 self.driver(description=element).click() # 点击定位元素 def click_text(self, element): # 点击,根据文本定位 self.driver(text=element).click() # 点击定位元素 def clear(self, element): # 清空输入框中的内容 if str(element).startswith("com"): # 若开头是com则使用ID定位 self.driver(resourceId=element).clear_text() # 清除文本 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 self.driver.xpath(element).clear_text() # 清除文本 else: # 若以上两种情况都不是,则使用描述定位 self.driver(description=element).clear_text() # 清除文本 def find_elements(self, element, timeout=5): # 找元素 is_exited = False try: while timeout > 0: xml = self.driver.dump_hierarchy() # 获取网页层次结构 if re.findall(element, xml): is_exited = True break else: timeout -= 1 except: print("元素未找到!") finally: return is_exited def assert_exited(self, element): # 断言元素是否存在 assert self.find_elements(element) == True, "断言失败,{}元素不存在!".format(element)
然后创建封装相应页面操作的方法,比如登录操作,创建login_page.py
文件,此层主要是封装操作流程
from base.basepage import BasePage # 导入基类中封装的方法 import allure # 导入allure class LoginPage(BasePage): # 元素定位,元素位置信息 agreement = "com.sankuai.meituan:id/permission_agree_btn" get_loc_info = "com.android.permissioncontroller:id/permission_allow_foreground_only_button" get_pho_perm = "com.android.permissioncontroller:id/permission_deny_button" notice = "暂不" login_in_now = "com.sankuai.meituan:id/button" pwd_login = "com.sankuai.meituan:id/user_password_login" username = "com.sankuai.meituan:id/passport_mobile_phone" password = "com.sankuai.meituan:id/edit_password" tick = "com.sankuai.meituan:id/passport_account_checkbox" click_login = "com.sankuai.meituan:id/login_button" mine = "我的" growth = "com.sankuai.meituan:id/grouth_tv" # 行为,页面操作 @allure.story('测试美团APP') @allure.title("APP登录") def login(self,user,pwd): # 登录流程,以首次运行APP为例 self.click(self.agreement) # 同意使用APP协议 self.click(self.get_loc_info) # 获取位置信息,选择【使用时允许】 self.click(self.get_pho_perm) # 获取电话权限,选择【拒绝】 self.click_text(self.notice) # 是否开启消息通知,选择【暂不】 self.click(self.login_in_now) # 点击首页的【马上登录】 self.click(self.pwd_login) # 登录方式选择【密码登录】 self.input(self.username,user) # 输入用户名 self.input(self.password,pwd) # 输入密码 self.click(self.tick) # 勾选同意用户协议 self.click(self.click_login) # 点击【登录】 self.click(self.mine) # 点击【我的】 self.assert_exited(self.growth) # 断言,因登录成功才显示成长值,故以此元素是否出现判断是否登录成功 self.click(self.homepage) # 切到首页
最后传参并执行用例操作,因用Pytest框架,所以此文件注意格式,文件名需以test开头,创建test_login.py
文件
import uiautomator2 as u2 import pytest from pageobject.login_page import LoginPage # 导入上一步操作流程中的类 # 连接手机并启动APP进行登录 class TestLogin: # 类以Test开头 def test_login(self): # 方法以test_或_test开头 driver = u2.connect("48fd6742") driver.app_start("com.sankuai.meituan", stop=True) login_page = LoginPage(driver=driver) login_page.login("16666666666","123456abc")
至此登录操作就算完成,执行测试就可以啦!
搜索操作也是相同的操作,把公共方法直接写入到基类文件中,操作流程单独创建search_page.py
文件
from base.basepage import BasePage # 导入基类,直接调用公共方法 import allure class SearchPage(BasePage): # 元素定位信息 inputfield = "com.sankuai.meituan:id/search_edit_flipper_container" clicksearch = "com.sankuai.meituan:id/search" inputvalue = "com.sankuai.meituan:id/search_edit" back = "com.sankuai.meituan:id/back" allure.title("搜索") def search(self,value): # 搜索操作流程 self.click(self.inputfield) # 点击搜索框 self.clear(self.inputvalue) # 每次搜索前线清空搜索框 self.input(self.inputvalue,value) # 定位输入框并输入关键字 self.click(self.clicksearch) # 点击【搜索】按钮 self.click(self.back) # 返回到上一级 self.click(self.back) # 返回到首页
创建传参及执行搜索操作的文件test_search.py
import uiautomator2 as u2 import pytest from pageobject.search_page import SearchPage # 导入上一步操作流程中的类 class TestSearch: keyword = ["奶茶","桌球","景点"] @pytest.mark.parametrize("value",keyword) # 使用pytest参数化,搜索三个关键字 def test_search(self,value): driver = u2.connect("48fd6742") driver.app_start("com.sankuai.meituan") search_page = SearchPage(driver=driver) search_page.search(value)
使用allure执行测试并生成报告
pytest --alluredir=./result testcases # 执行testcases文件下所有测试用例,并将中间结果生成到result目录中 allure serve ./result # 生成最终报告
关于Pytest、Allure的使用请查看此文章
关于PO模型请查看此文章