- 安装
pip install appium-python-client
一、capability 配置参数解析
1、 Session
- Appium 的客户端和服务端之间进行通信的前提;
- 通过
Desired Capabilities
建立会话;
2、 Capability 简介
- 功能:配置 Appium 会话,告诉 Appium 服务器需要自动化的平台及应用程序的相关信息;
- 形式:键值对的集合,键对应设置的名称,值对应设置的值;
- 主要分为三部分:
- 公共部分;
- ios 部分;
- android 部分;
3、公共部分参数配置
- 注意:appium2.x
键 | 描述 | 值 |
---|---|---|
platformName |
使用的手机操作系统 | iOS,Android,或者 Firefox0S |
platformVersion |
手机操作系统的版本 | 例如 7.1 , 4.4
|
deviceName |
使用的手机或模拟器类型 |
iPhone Simulator , iPad Simulator , iPhone Retina 4-inch , Android Emulator , Galaxy S4 , 等等…. 在 iOS 上,使用 Instruments的 instruments -s devices 命令可返回一个有效的设备的列表。在 Andorid 上虽然这个参数目前已被忽略,但仍然需要添加上该参数 |
automationName |
使用哪个自动化引擎 |
android 使用uiautomator2 ,ios 使用XCUTest ;注意:appium2.x中必须指定这个参数
|
noReset |
在当前 session 下不会重置应用的状态–也就是不清理应用的缓存信息。默认值为 false
|
true , false
|
udid |
连接的真实设备的唯一设备编号 (Unique device identifier) | 例如 1ae203187fc012g
|
4、 Android 部分特有参数配置
键 | 描述 | 值 |
---|---|---|
appActivity | Activity 的名字是指从你的包中所要启动的 Android acticity。他通常需要再前面添加. (例如 使用 .MainActivity 代替 MainActivity)----注意:目前看来只能是app的启动页面
|
.view.WelcomeActivityAlias |
appPackage | 运行的 Android 应用的包名 | com.xueqiu.android |
appWaitActivity | 用于等待启动的 Android Activity 名称 | SplashActivity |
unicodeKeyboard | 启用 Unicode 输入字符串–也就是可以输入中文,默认为 false | true or false |
resetKeyboard | 是否隐藏键盘 | true or false |
dontStopAppOnReset | 首次启动的时候,不停止 app | true or false |
skipDeviceInitialization | 跳过安装,权限设置等操作 | true or false |
5、 iOS 部分特有参数配置
键 | 描述 | 值 |
---|---|---|
bundleId | 被测应用的 bundle ID 。用于在真实设备中启动测试,也用于使用其他需要 bundle ID 的关键字启动测试。在使用 bundle ID 在真实设备上执行测试时,你可以不提供 app 关键字,但你必须提供 udid 。 | 例如 io.appium.TestApp |
autoAcceptAlerts | 当 iOS 的个人信息访问警告 (如 位置、联系人、图片) 出现时,自动选择接受( Accept )。默认值 false | true 或者 false |
showIOSLog | 是否在 appium 日志中显示从设备捕获的任何日志。默认 false | true or false |
6、capability配置进阶用法
- 注意:这是appium1.x版本的配置,针对appium2.x多少有些改动,具体查看官方文档
键 | 描述 | 值 | 说明 |
---|---|---|---|
udid | 多设备选择的时候,要指定 udid; 默认读取设备列表的第一个设备 | 设备id,Android:adb devices 查看 |
appium2.x中好像直接用deviceName |
newCommandTimeout | appium 程序应等待来自客户端的新命令多长时间;超时后==会话删除==; 默认 60 秒;设置为 0 禁用 |
||
autoGrantPermissions | 授予启动的应用程序某些权限 | 在appium2.x的官方文档中没有找到 | |
PRINT_PAGE_SOURCE_ON_FIND_FAILURE | 默认为 false ,发生任何错误,强制服务器将实际的 XML 页面源转储到日志中. |
在appium2.x的官方文档中没有找到 | |
fullReset | 默认为 false ,true :新会话之前完全卸载被测应用程序; *安卓:在会话开始之前(appium 启动 app)和测试之后停止应用程序,清除应用程序数据并卸载 apk |
||
dontStopAppOnReset | 默认为 false ,不希望应用程序在运行时重新启动,设置为 true ;好处:上一条用例和下一条用例衔接的时候页面保持当前页面 |
在appium2.x的官方文档中没有找到 |
-
说明,dontStopAppOnReset参数ture和false时的具体作用就相当于可以下两条adb命令:
- 打开的app退出后重新启动:
adb shell am start -S 包名/activity名
- 打开的app不需要退出,直接使用当前页面:
adb shell am start 包名/activity名
-
注意:
- 1、如果在app的manifest配置中声明了
android:exproted=false
那么通过以上命令是无法启动app的; - 2、还有一种情况获取的页面名不对,此时可以使用命令获取包的信息去找到页面名:
adb shell dumpsys package 包名
—但是就算获取了包的信息页面名也很难找到!!!因为信息太多!
- 1、如果在app的manifest配置中声明了
- 打开的app退出后重新启动:
7、 appium 2.0 + 版本使用了noReset 参数之后无法启动app或者不能关闭app的解决方案
-
问题: 在启动参数使用了noReset 参数之后,可能会出现不会调起app,然后在teardown的时候加上了
driver.quit()
也无法关闭app; - 解决方案: 在参数上再追加以下两个参数。
# 设置以下两个参数来控制启动app和关闭掉app
caps["appium:forceAppLaunch"] = True
caps["appium:shouldTerminateApp"] = True
二、启动退出app
-
注意:appium2.x的用法发生了很大的变化!!!
-
启动:
webdriver.Remote(appium_server_url,options=UiAutomator2Options().load_capabilities(capabilities))
-
退出:
driver.quit()
from appium.options.android import UiAutomator2Options
from appium.webdriver import webdriver
from appium.webdriver.common.appiumby import AppiumBy
# 与appium建立连接配置
capabilities = dict(
platformName='Android',
automationName='uiautomator2',
deviceName='Android',
appPackage='com.android.settings',
appActivity='.Settings',
)
# appium服务器地址
appium_server_url = 'http://127.0.0.1:4723'
class TestAppium:
def setup_class(self) -> None:
# 传递服务器地址和连接配置创建连接启动app
self.driver = webdriver.Remote(appium_server_url, options=UiAutomator2Options().load_capabilities(capabilities))
def teardown_class(self) -> None:
if self.driver:
# 退出app
self.driver.quit()
def test_find_battery(self) -> None:
# 通过resource-id定位WLAN
el = self.driver.find_element(by=AppiumBy.ID, value='com.android.settings:id/tile_divider')
# 点击
el.click()
1、为什么是Remote
- 因为在模块的
__init__.py
文件中进行了重命名;
2、capability为什么要通过options传递
-
说白了,和appium1.x的用法不一样,这是appium2.x中启动Android的固定写法;
- appium2.x的Android驱动是UiAutomator2,所以用UiAutomator2Options对象的方法load_capabilities()加载capability;
-
上源码:
def __init__(
self,
command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub',
keep_alive: bool = True,
direct_connection: bool = True,
extensions: Optional[List['WebDriver']] = None,
strict_ssl: bool = True,
options: Union[AppiumOptions, List[AppiumOptions], None] = None,
):
三、元素定位基础知识
- 概念:元素定位的含义就是定位控件;
- 注意:同一脚本同时支持 android/iOS 两个系统的前提是元素属性(id,aid,xpath 等)一致;
1、Android/iOS基础知识
(1)、 android 基础知识
-
Android 是通过容器的布局属性来管理子控件的位置关系,布局关系就是把界面上的所有的空间,根据他们的间距的大小,摆放在正确的位置。
-
布局
- 是可用于放置很多控件的容器按照一定的规律调整内部控件的位置由此构成界面。
- 嵌套布局
- 布局内部放置布局,多层布局嵌套,可以完成复杂的界面结构。
- Android 七大布局
- LinerLayout(线性布局)
- RelativeLayout(相对布局)
- FrameLayout(帧布局)
- AboluteLayout(绝对布局)
- TableLayout(表格布局)
- GridLayout(网格布局)
- ConstraintLayout(约束布局)
-
Android 四大组件
- activity 与用户交互的可视化界面;
- service 实现程序后台运行的解决方案;
- content provider 内容提供者,提供程序所需要的数据;
- broadcast receiver 广播接收器,监听外部事件的到来(比如来电);
-
常用的控件
- TextView(文本控件),EditText(可编辑文本控件)
- Button(按钮),ImageButton(图片按钮),ToggleButton(开关按钮)
- ImageView(图片控件)
- CheckBox(复选框控件),RadioButton(单选框控件)
(2)iOS基础知识
- 布局
- iOS 不使用布局的概念,用变量之间的相对关系完成位置的计算;
- 注意:使用 Appium 测试 iOS 应用需要使用 MacOS 操作系统;
2、 控件基础知识
(1)前端通用
- dom:Document Object Model 文档对象模型
- dom 应用:用于表示界面的控件层级,界面的结构化描述
- 常见的格式:html、xml
- 核心元素:节点、属性
- xpath:xml 路径语言,用于 xml 中的节点定位
(2)Android区别
- Anrdroid 应用的层级结构与 html 不一样,是一个定制的 xml
- app source 类似于 dom ,表示 app 的层级,代表了界面里面所有的控件树的结构
- 每个控件都有它的属性(resourceid,xpath,aid),但是没有 css 属性
- clickable
- content-desc
- resource-id
- text
- bounds
需求app登录页面xml源码:
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy index="0" class="hierarchy" rotation="0" width="720" height="1280">
<android.widget.FrameLayout index="0" package="com.xueqiu.android" class="android.widget.FrameLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,0][720,1280]" displayed="true">
<android.widget.LinearLayout index="0" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,0][720,1280]" displayed="true">
<android.widget.FrameLayout index="0" package="com.xueqiu.android" class="android.widget.FrameLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,0][720,1280]" displayed="true">
<android.view.ViewGroup index="0" package="com.xueqiu.android" class="android.view.ViewGroup" text="" resource-id="com.xueqiu.android:id/decor_content_parent" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,0][720,1280]" displayed="true">
<android.widget.FrameLayout index="0" package="com.xueqiu.android" class="android.widget.FrameLayout" text="" resource-id="android:id/content" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,0][720,1280]" displayed="true">
<android.widget.FrameLayout index="1" package="com.xueqiu.android" class="android.widget.FrameLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,36][720,1280]" displayed="true">
<android.widget.FrameLayout index="0" package="com.xueqiu.android" class="android.widget.FrameLayout" text="" resource-id="com.xueqiu.android:id/fl_outside" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,36][720,1280]" displayed="true" />
<android.widget.LinearLayout index="1" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,36][720,1280]" displayed="true">
<android.widget.RelativeLayout index="0" package="com.xueqiu.android" class="android.widget.RelativeLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,36][720,438]" displayed="true">
<android.widget.RelativeLayout index="0" package="com.xueqiu.android" class="android.widget.RelativeLayout" text="" resource-id="com.xueqiu.android:id/sky_container" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,36][720,269]" displayed="true">
<android.widget.ImageView index="0" package="com.xueqiu.android" class="android.widget.ImageView" text="" resource-id="com.xueqiu.android:id/iv_action_back" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[633,36][720,138]" displayed="true" />
<android.widget.TextView index="1" package="com.xueqiu.android" class="android.widget.TextView" text="聪明的投资者,你好" resource-id="com.xueqiu.android:id/tv_title" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,159][426,216]" displayed="true" />
</android.widget.RelativeLayout>
<android.widget.LinearLayout index="1" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" resource-id="com.xueqiu.android:id/register_module" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,269][720,438]" displayed="true">
<android.widget.LinearLayout index="0" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" resource-id="com.xueqiu.android:id/register_module" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,269][720,438]" displayed="true">
<android.widget.RelativeLayout index="0" package="com.xueqiu.android" class="android.widget.RelativeLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,269][672,353]" displayed="true">
<android.widget.LinearLayout index="0" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" resource-id="com.xueqiu.android:id/register_number_start" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,269][138,353]" displayed="true">
<android.widget.TextView index="0" package="com.xueqiu.android" class="android.widget.TextView" text="+86" resource-id="com.xueqiu.android:id/register_number_start_text" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,269][87,353]" displayed="true" />
<android.widget.ImageView index="1" package="com.xueqiu.android" class="android.widget.ImageView" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[96,302][114,320]" displayed="true" />
</android.widget.LinearLayout>
<android.widget.EditText index="1" package="com.xueqiu.android" class="android.widget.EditText" text="请输入手机号" resource-id="com.xueqiu.android:id/register_phone_number" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="true" long-clickable="true" password="false" scrollable="false" selected="false" bounds="[137,269][672,353]" displayed="true" />
<android.widget.TextView index="2" package="com.xueqiu.android" class="android.widget.TextView" text="获取验证码" resource-id="com.xueqiu.android:id/register_code_text" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[522,291][672,330]" displayed="true" />
</android.widget.RelativeLayout>
<android.view.View index="1" package="com.xueqiu.android" class="android.view.View" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,353][672,354]" displayed="true" />
<android.widget.LinearLayout index="2" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,354][672,438]" displayed="true">
<android.widget.TextView index="0" package="com.xueqiu.android" class="android.widget.TextView" text="验证码" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,379][138,412]" displayed="true" />
<android.widget.EditText index="1" package="com.xueqiu.android" class="android.widget.EditText" text="请输入四位验证码" resource-id="com.xueqiu.android:id/register_code" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" long-clickable="true" password="false" scrollable="false" selected="false" bounds="[138,354][654,438]" displayed="true" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</android.widget.RelativeLayout>
<android.view.View index="1" package="com.xueqiu.android" class="android.view.View" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,438][672,439]" displayed="true" />
<android.widget.CheckBox index="2" package="com.xueqiu.android" class="android.widget.CheckBox" text="阅读并同意 服务协议、隐私政策" resource-id="com.xueqiu.android:id/service_agreement" checkable="true" checked="false" clickable="true" enabled="true" focusable="true" focused="false" long-clickable="true" password="false" scrollable="false" selected="false" bounds="[48,457][328,482]" displayed="true" />
<android.widget.TextView index="3" package="com.xueqiu.android" class="android.widget.TextView" text="登录" resource-id="com.xueqiu.android:id/button_next" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,500][672,566]" displayed="true" />
<android.widget.LinearLayout index="4" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" resource-id="com.xueqiu.android:id/login_more" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,578][672,627]" displayed="true">
<android.widget.LinearLayout index="0" package="com.xueqiu.android" class="android.widget.LinearLayout" text="" resource-id="com.xueqiu.android:id/ll_login_normal" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,578][672,627]" displayed="true">
<android.widget.TextView index="0" package="com.xueqiu.android" class="android.widget.TextView" text="邮箱验证码登录" resource-id="com.xueqiu.android:id/login_without_password" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[48,578][203,627]" displayed="true" />
<android.view.View index="1" package="com.xueqiu.android" class="android.view.View" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[203,578][541,579]" displayed="true" />
<android.widget.TextView index="2" package="com.xueqiu.android" class="android.widget.TextView" text="帐号密码登录" resource-id="com.xueqiu.android:id/login_outside" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[541,578][672,627]" displayed="true" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
<android.view.View index="5" package="com.xueqiu.android" class="android.view.View" text="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" long-clickable="false" password="false" scrollable="false" selected="false" bounds="[0,627][720,1280]" displayed="true" />
</android.widget.LinearLayout>
</android.widget.FrameLayout>
</android.widget.FrameLayout>
</android.view.ViewGroup>
</android.widget.FrameLayout>
</android.widget.LinearLayout>
</android.widget.FrameLayout>
</hierarchy>
(3) iOS 与 Android dom 结构的区别
- dom 属性和节点结构类似
- 名字和属性命名不同
- android 的 resourceid 和 ios 的 name
- android 的 content-desc 和 ios 的 accessibility-id
3、 选择定位器通用原则
- 与研发约定的属性优先
- android 推荐 content-description
- ios 推荐 label
- 身份属性 id
- 组合定位 xpath,css
- 其它定位
四、appium元素定位方法
- Appium 提供多种元素定位方式,id,xpath, class, 也可以通过 Android Uiautomator 定位,或 iOS Predicate
- xpath 是比较灵活的定位方式
1、源码中给出的定位方法
from selenium.webdriver.common.by import By
class AppiumBy(By):
IOS_PREDICATE = '-ios predicate string'
IOS_UIAUTOMATION = '-ios uiautomation'
IOS_CLASS_CHAIN = '-ios class chain'
ANDROID_UIAUTOMATOR = '-android uiautomator'
ANDROID_VIEWTAG = '-android viewtag'
ANDROID_DATA_MATCHER = '-android datamatcher'
ANDROID_VIEW_MATCHER = '-android viewmatcher'
# Deprecated
WINDOWS_UI_AUTOMATION = '-windows uiautomation'
ACCESSIBILITY_ID = 'accessibility id'
IMAGE = '-image'
CUSTOM = '-custom'
定位策略 | 描述 |
---|---|
Accessibility ID | 识别一个唯一的 UI 元素,对于 XCUITest 引擎,它对应的的属性名是 accessibility-id,对于 Android 系统的页面元素,对应的属性名是 content-desc |
Class name | 对于 iOS 系统,它的 class 属性对应的属性值会以XCUIElementType开头,对于 Android 系统,它对应的是 UIAutomator2 的 class 属性(e.g.: android.widget.TextView) |
ID | 原生元素的标识符,Android 系统对应的属性名为resource-id,iOS 为name |
Name | 元素的名称 |
XPath | 使用 xpath 表达式查找页面所对应的 xml 的路径(不推荐,存在性能问题) |
Image | 通过匹配 base 64 编码的图像文件定位元素 |
Android UiAutomator (UiAutomator2 only) | 使用 UI Automator 提供的 API, 尤其是 UiSelector 类来定位元素,在 Appium 中,会发送 Java 代码作为字符串发送到服务器,服务器在应用程序的环境中执行这段代码,并返回一个或多个元素 |
Android View Tag (Espresso only) | 使用 view tag 定位元素 |
Android Data Matcher (Espresso only) | 使用 Espresso 数据匹配器定位元素 |
IOS UIAutomation | 在 iOS 应用程序自动化时,可以使用苹果的 instruments 框架查找元素 |
2、Android原生定位
(1)Android原生类UiSelector介绍
A、 作用:通过各种属性与节点关系定位组件
B 、方法
方法名 | 描述 | 说明 |
---|---|---|
checkable(boolean val) | 设置搜索条件以匹配可检查的小组件。 | |
checked(boolean val) | 设置搜索条件以匹配当前选中的小组件(通常用于复选框)。 | |
childSelector(UiSelector selector) | 将子 UiSelector 条件添加到此选择器。 | 常用-父子结点定位 |
className(String className) | 设置搜索条件以匹配微件的类属性(例如,“android.widget.Button”)。 | |
className(Class type) | 设置搜索条件以匹配微件的类属性(例如,“android.widget.Button”)。 | |
classNameMatches(String regex) | 使用正则表达式设置搜索条件以匹配小组件的类属性。 | |
clickable(boolean val) | 设置搜索条件以匹配可单击的小组件。 | |
description(String desc) | 设置搜索条件以匹配小组件的 content-description 属性。 | |
descriptionContains(String desc) | 设置搜索条件以匹配小组件的 content-description 属性包含什么内容。 | |
descriptionMatches(String regex) | 使用正则匹配content-description 属性。 | |
descriptionStartsWith(String desc) | content-description 属性以什么开头。 | |
enabled(boolean val) | 设置搜索条件以匹配可操作的小组件。 | |
focusable(boolean val) | 设置搜索条件以匹配可聚焦的小组件。 | |
focused(boolean val) | 设置搜索条件以匹配具有焦点的小组件。 | |
fromParent(UiSelector selector) | 将子 UiSelector 条件添加到此选择器,该选择器用于从父小组件开始搜索。 | 常用-兄弟元素定位 |
index(int index) |
设置搜索条件,使其按布局层次结构中的节点索引匹配小组件。 | |
instance(int instance) | 设置搜索条件以按其实例id匹配小组件。 | |
longClickable(boolean val) | 设置搜索条件以匹配可长按点击的小组件。 | |
packageName(String name) | 设置搜索条件以匹配包含小组件的应用程序的包名称。 | |
packageNameMatches(String regex) | ||
resourceId(String id) | 设置搜索条件以匹配给定的资源 ID。 | |
resourceIdMatches(String regex) | 使用正则表达式设置搜索条件以匹配小组件的资源 ID。 | |
scrollable(boolean val) |
设置搜索条件以匹配可滚动的小组件。 | |
selected(boolean val) | 设置搜索条件以匹配当前选择的小组件。 | |
text(String text) | 设置搜索条件以匹配小组件中显示的可见文本(例如,用于启动应用程序的文本标签)。 | 常用 |
textContains(String text) | 设置搜索条件以匹配小组件中的可见文本,其中可见文本必须包含输入参数中的字符串。 | 常用 |
textMatches(String regex) | 使用正则表达式设置搜索条件以匹配布局元素中显示的可见文本。 | 常用 |
textStartsWith(String text) | 设置搜索条件以匹配以 text 参数为前缀的小组件中的可见文本。 | 常用 |
C、Android组件属性介绍
(2) Android 原生定位 - 单属性定位
- 格式
'new UiSelector().属性名("<属性值>")'
- 比如:
'new UiSelector().resourceId("android:id/text1")'
- 比如:
- 注意外面是单引号,里面是双引号,顺序不能变
- 可以简写为
属性名("<属性值>")'
- 比如:·
resourceId("android:id/text1")
- 比如:·
# ID 定位
def test_android_uiautomator_by_id(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().resourceId("android:id/text1")'))
# TEXT 定位
def test_android_uiautomator_by_text(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("App")'))
# classname 定位
def test_android_uiautomator_by_className(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().className("android.widget.TextView")'))
(3) Android 原生定位-组合定位
- 多个属性同时确定元素的(多个属性任意组合 ,不限长度),链式调用;
driver.find_element_by_android_uiautomator('\
new UiSelector().resourceId("com.xueqiu.android:id/tab_name").\
text("我的")')
(4) Android 原生定位-模糊匹配
- 文字包含
- 文字以 x 开头
- 文字正则匹配
# 模糊匹配--包含
def test_android_uiautomator_by_text_contains(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().textContains("ssi")').text)
# 模糊匹配--以什么开头
def test_android_uiautomator_by_text_start_with(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().textStartsWith("Ani")').text)
# 模糊匹配--正则匹配
def test_android_uiautomator_by_text_match(self):
print(self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().textMatches("^Pre.*")').text)
(5) Android 原生定位-层级定位
- 兄弟元素定位
fromParent
- 父子结点定位
childSelector
, 可以传入 resourceId() , description() 等方法
#兄弟元素定位 fromParent: 查找目标元素Text,先找App ,fromParent() 方法可以查找兄弟结点
new UiSelector().text("App").fromParent(text("Text"))
# 根据父结点查找子结点/ 子孙结点
new UiSelector().className("android.widget.ListView").childSelector(text("Text"))
# 雪球app首页-通过原生父子定位进行定位输入框--先定位到com.xueqiu.android:id/tv_banner,再定位下级的android.widget.TextView
selector = 'new UiSelector().resourceId("com.xueqiu.android:id/tv_banner").childSelector(className("android.widget.TextView"))'
input = self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,selector)
print(input.text)# 左侧机会!食品ETF(515710)
(6) 滑动查找元素
new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("查找的元素文本").instance(0))
(7)Android-toast定位
- appium 用的是uiautomator底层来抓取toast,
- 再把toast放到控件树内,但是它本身不属于空间
- 使用的是uiautomator2
- 使用xpath定位toast:
- 使用class属性:
//*[@class="android.widget.Toast"]
- 使用text属性模糊匹配:
[contains(@text,"xxx")]
, xxx:toast的文本内容;
- 使用class属性:
# 使用显示等待toast的出现--使用text属性模糊匹配
WebDriverWait(self.driver, 10).until(
expected_conditions.visibility_of_element_located((AppiumBy.XPATH, '//*[contains(@text,"添加")]')))
# 通过class属性定位toast
toast = self.driver.find_element(AppiumBy.XPATH, '//*[@class="android.widget.Toast"]')
# 获取toast文案
result = toast.text
print(f"toast文案:{result}")
五、appium元素操作
1、普通元素操作
-
element.click()
:点击元素 -
element.clear()
:清空输入框 -
element.send_keys(xx)
:输入操作–appium-python-client3.1版本的这个方法会自动清空输入框 -
element.text
:获得元素的text内容 -
element.get_attribute(‘属性名’)
:获取属性名对应的属性值- 方法能获取的属性,元素的属性几乎都能获取到,属性名称和 uiautomatorviewer 里面的一致,可以获取的属性如下
- resource-id/resourceld 返回 resource-id(API=>18 支持)
- text 返回 text
- class 返回 class(API=>18 支持)
- content-desc/contentDescription 返回 content-desc 属性
- checkable,checked,clickable,enabled,focusable,focused,{long-clickable,longClickable), package, password,scrollable,selection-start,selection-end,selected,bounds,displayed,contentSize 返回 true or false
- 方法能获取的属性,元素的属性几乎都能获取到,属性名称和 uiautomatorviewer 里面的一致,可以获取的属性如下
-
element.set_value('appium')
:设置元素的值 -
element.is_displayed()
:是否可见,返回 True/False -
element.is_enabled()
:是否可用,返回 True/False -
element.is_selected()
:是否被选中,返回 True/False -
element.size
:获取元素的大小(高和宽),{'width':500,'height':22}
-
element.location
:获取元素左上角的坐标,{'y': 19,'x: 498}
-
driver.get_window_size()
: 获取手机的分辨率 -
driver.page_source
:获取当前页面的源码 -
driver.quit()
:退出app并杀掉和appium服务链接的session -
driver.get_screenshot_as_file(“路径”)
:将手机屏幕截图并保存到文件 -
driver.open_notifications()
:打系统通知栏(仅支持API 18 以上的安卓系统)
案例:雪球app搜索功能
"""
登录【雪球】进入应用首页---使用noReset=true参数保持登录状态,登录的问题后续处理滑动验证码问题
点击搜索框(点击之前,判断搜索框的是否可用,并查看搜索框 name 属性值,并获取搜索框坐标,以及它的宽高)
向搜索框输入:alibaba
判断【阿里巴巴】是否可见
如果可见,打印“搜索成功”
如果不可见,打印“搜索失败
"""
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
class TestSearch:
def setup_class(self):
# capability配置
caps = {}
caps["platformName"] = "Android"
caps["appium:platformVersion"] = "6.0.1"
caps["appium:deviceName"] = "127.0.0.1:7555"
caps["appium:appPackage"] = "com.xueqiu.android"
caps["appium:appActivity"] = ".view.WelcomeActivityAlias"
caps["appium:automationName"] = "UiAutomator2"
caps["appium:unicodeKeyboard"] = "true"
caps["appium:restKeyboard"] = "true"
caps["appium:noReset"] = "true"
# appium服务器地址
appium_server_url = 'http://127.0.0.1:4723'
self.driver = webdriver.Remote(appium_server_url,options=UiAutomator2Options().load_capabilities(caps=caps))
self.driver.implicitly_wait(10)
def teardown_class(self):
self.driver.quit()
def test_search(self):
# 定位搜索输入框并输入内容
# 通过原生父子定位进行定位输入框--先定位到com.xueqiu.android:id/tv_banner,再定位下级的android.widget.TextView
selector = 'new UiSelector().resourceId("com.xueqiu.android:id/tv_banner").childSelector(className("android.widget.TextView"))'
input = self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,selector)
print(input.text)# 左侧机会!食品ETF(515710)
# 点击输入框进入搜索页面
input.click()
# 定位搜索页面输入框并输入内容
self.driver.find_element(AppiumBy.ID,"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
# 点击搜索按钮
self.driver.find_element(AppiumBy.ID,"com.xueqiu.android:id/search_btn").click()
# 获取实际结果
# 定位一组信息
stocks = self.driver.find_elements(AppiumBy.ID,"com.xueqiu.android:id/stock_name_tv")
print(stocks[0].text)
assert "阿里巴巴" in stocks[0].text
2、 高级控件交互-ActionChains
3、高级控件交互-ActionHelpers
-
说明:WebDirver类继承了ActionHelpers类;
- driver.scroll():从一个元素滚动到另一个元素。
- driver.drag_and_drop():将 origin 元素拖动到 destination 元素。
- driver.tap():用最多五根手指敲击特定位置,按住一定时间。
- driver.swipe():从一个点滑动到另一个点,持续时间可选。
- driver.flick():从一个点轻拂到另一个点。
六、 appium设备交互API
七、元素定位工具合集
1、Android Sdk自带工具 uiautomatorviewer
(1)工具位置
- 打开Android Sdk
tools/bin
目录下的uiautomatorviewer
程序; - 注意:Android8及以上不支持;
(2) uiautomatorviewer 工具功能介绍
- 第一个是通过分析给定的文件定位
- 第二个是将当前界面截图并分析xml结构
- 第三个与第二个功能类似,但它会对页面内容进行压缩,导致一些控件定位不准确
- 第四个是保存当前界面的截图以及xml结构
2、weditor
(1)weditor 是 python 的第三方库
- 要求:python 3.6+ 以上
- 安装:
pip install weditor
- 运行:安装完成之后,在命令行运行
python -m weditor
即可
(2) weditor 功能介绍
- 支持 Android 和 iOS 的界面分析
- 通过设备的 uuid 连接设备
- 展示页面结构
3、appium inspector
八、POM设计模式
1、POM设计设计原则
- 字段意义
- 不要暴露页面内部的元素给外部
- 不需要建模 UI 内的所有元素
- 方法意义
- 用公共方法代表 UI 所提供的功能
- 方法应该返回其他的 PageObject 或者返回用于断言的数据
- 同样的行为不同的结果可以建模为不同的方法
- 不要在方法内加断言
2、如何编码
3、多app的basepage如何封装
-
basepage:封装所有app都会用到的公共方法以及初始化方法,在封装的时候可以用到什么加入什么,不用先把能想到的都封装在basepage中,basepage一定要防止文件过大内容过多;
- 构造方法的作用:因为所有的page页面都需要用到driver,在实例化的时候需要传过去,所以basepage中的构造方法的主要作用就是解决每个页面初始化的driver问题;
-
app_page:每个app的管理page,继承自basepage,里面包含各自app的处理,比如:start、stop、restart、goto_homepage等;
-
业务page:继承自app_page,封装页面内的功能和属性;
-
注意:如果出现循环导入的问题,那就将其中一个导入放到具备导入,不要放在文件头部做全局导入;
4、案例
需求: 雪球app搜索场景
- 打开【雪球】应用首页
- 点击搜索框,进入搜索页面
- 向搜索输入框中输入【alibaba】
- 点击搜索结果中的【阿里巴巴】
- 切换到 tab 的【股票】
- 找到 股票【阿里巴巴】的股票价格 price
- 判断 price 在 110 上下 10%浮动
九、 自动化测试架构
1、 打造测试框架的需求与价值
- 领域模型适配:封装业务实现,实现业务管理
- 提高效率:降低用例维护成本,提高执行效率
- 增强功能:解决已有框架不满足的情况
2、 自动化框架应具备的功能
- 支持管理用例,运行用例
- 支持查找元素/定位元素,对元素/页面 进行各种操作(点击,滑动,输入等等)
- 支持生成测试报告
- 能够实现功能的复用,(比如登录,搜索等)
- 当页面有异常弹框的时候,可以进行有效的处理
- 当用例失败,需要添加失败时的日志,截图,等信息,放在测试报告中
- 多设备并发
- 支持平台化
- 其他…
3、 自动化测试框架实现
功能 | 实现 |
---|---|
管理用例,运行用例 | pytest |
查找元素/定位元素 | Appium |
测试报告 | Allure |
功能复用 | PO 实现 |
异常弹框 | 编写代码 |
失败时的日志,截图 | 编写代码 |
多设备并发 | selenium grid |
平台化 | VUE+FLASK/Django |
4、 项目结构
5、 框架封装
自动化测试框架优化(在 PO 的基础上,添加异常处理,日志,报告 ,截图,参数化与数据驱动等)逐步的将框架完善
- 异常处理(弹窗黑名单)
# 声明一个黑名单
black_list = [(),(),()]
def black_wrapper(fun):
def run(*args, **kwargs):
basepage = args[0]
try:
return fun(*args, **kwargs)
except Exception as e:
for black in black_list:
eles = basepage.driver.find_elements(*black)
if len(eles) > 0:
eles[0].click()
return fun(*args, **kwargs)
raise e
return run
@black_wrapper
def find(self, by, locator)
return self.driver.find_element(by, locator)
-
日志记录
- 运行日志记录
- 错误日志记录
-
报告生成
- 异常日志
- 异常截图
- 测试用例步骤
- 测试描述
- bug,issue 关联
- 用例分类(feature,story,step 等)
-
参数化与数据驱动
- 支持测试用例 / 步骤层级的参数化驱动配置
- 配置方式包括三个部分
- 参数定义(指定名字)
- 数据源指定(指定 yaml 文件 /或者其它格式文件)
- 数据源准备(无论是从线上环境 捞的数据,还是自己创建的测试数据)