目录:
1.app自动化测试的价值与体系
UI 自动化价值
提高效率
提高质量:
app 测试的时代背景
技术选型
自动化测试框架选择
UI 自动化测试用例如何编写
技术选型 1
技术选型 2
学习路线:
2.环境安装与使用
目前 mobile 自动化解决方案
iOS | Android |
---|---|
Calabash-iOS | calabash-Android |
Frank | MonkeyTalk |
UIAutomation | Robotium |
iOS-driver | UIAutomator |
KeepItFunctional | Selendroid |
Macaca | Macaca |
Appium | Appium |
自动化工具选择
工具名称 | 被测系统 | 脚本语言 | 是否支持 H5 | 是否支持跨应用 | 工具稳定性 | 是否为 sdk 自带 |
---|---|---|---|---|---|---|
MonkeyRunner | Android | Y | N | 稳定 | Y | |
Monkey | Android | Java | N | N | 稳定 | Y |
Uiautomator2 | Android | Java | Y | Y | 稳定 | Y |
Uiautomation | iOS | 弃用 | ||||
Adb-For-Test | Android | Java/Python | Y | Y | 稳定 | Y |
Appium | Android,iOS | Java/Python/JS/C# | Y | Y | 一般 | N |
Appium介绍
Appium是一个移动端的自动化测试框架,可用于测试原生应用,移动网页应用和混合应用﹐且是跨平台的。可用于iOS和Android操作系统原生应用是指用android或iOS编写的应用﹐移动网页应用是指网页应用﹐类似于iOS中safari应用或者Chrome应用或著类似浏览器的应用。混合应用是指一种包裹webview的应用,原生应用网页内容交互性的应用。重要的是Appium是跨平台的。何为跨平台,意思是可以针对不同平台用一套api来编写脚本。
appium生态工具
Appium安装-简化版
安装配置成功后的截图:
jdk1.8:
android sdk:
Appium Desktop
github 下载版本 1.19.1 不分开
release
无需环境变量配置
Appium python client:
pip install appium-python-client
第一个appium测试用例:
1.先连接mumu模拟器
2.编写脚本:
from appium import webdriver
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
# com.android.settings/com.android.settings.Settings
desired_caps['appPackage'] = 'com.android.settings'
desired_caps['appActivity'] = 'com.android.settings.Settings'
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
print("启动【设置】应用")
driver.quit()
运行结果:
3.自动化用例录制
1.先连接mumu模拟器
2.打开Appium Inspector
Appium Inspector 功能介绍
用例录制
获取 app 的信息
adb logcat ActivityManager:I | grep "cmp"
adb logcat ActivityManager:I | findstr "cmp"
aapt dump badging wework.apk | grep launchable-activity
aapt dump badging wework.apk | findstr launchable-activity
adb shell am start -W -n / -S
配置待测应用
platformName
:平台,Android/iOSdeviceName
:设备名appPackage
:应用的包名appActivity
:应用的页面名 ActivitynoReset
: 防止清空缓存信息功能键
SelectElements
:选中元素,查看层级和属性Swipe By Coordinates
:通过坐标点滑动Tap By Coordinates
:通过坐标点点击Back
:返回Refresh Source & Screenshot
:刷新页面StartRecording
:开始录制脚本Search for element
:搜索元素Copy XML Source to Clipboard
:复制 xml 结构Quit Session & Close Inspector
:退出当前 Session示例:
1.先获取包名和页面名
2.填写对应的参数,运行:
3.运行结果:
点击录制按钮,可以记录接下来的操作,和selenium差不多,可以导出python脚本。。。
4.自动化测试用例结构分析
desktop 生成用例脚本
el1
:点击 OS
,进入下一个页面el2
:点击 Morse Code
sendkeys
方法,输入baidu.com
el1 = driver.find_element_by_accessibility_id("OS")
el1.click()
el2 = driver.find_element_by_accessibility_id("Morse Code")
el2.click()
el3 = driver.find_element_by_id("io.appium.android.apis:id/text")
el3.clear()
el3.send_keys("baidu.com")
driver.back()
driver.back()
用例脚本优化
webdriver
,添加setup
和teardown
noReset
属性增强用例稳定性import time
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
class TestXueQiu:
def setup(self):
desire_cap = {}
# 平台
desire_cap['platform'] = 'Android'
# 设备名
desire_cap['deviceName'] = 'emulator'
# app 包名
desire_cap['appPackage'] = 'io.appium.android.apis'
# app 页面名
desire_cap['appActivity'] = '.ApiDemos'
self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desire_cap)
self.driver.implicitly_wait(10)
def teardown(self):
time.sleep(3)
# 退出应用
self.driver.quit()
def test_api_demo(self):
"""
1、打开 API demo apk
2、点击 OS 控件
3、点击 Morse Code 控件
4、在搜索框中输入 ceshiren.com
5、返回到第一页
6、断言
:return:
"""
# 点击 OS 控件
self.driver.find_element_by_accessibility_id("OS").click()
# 点击 Morse Code 控件
self.driver.find_element_by_accessibility_id("Morse Code").click()
# 输入`ceshiren.com`
self.driver.find_element_by_id("io.appium.android.apis:id/text").clear()
self.driver.find_element_by_id("io.appium.android.apis:id/text").send_keys("baidu.com")
# 返回第一页
self.driver.back()
self.driver.back()
self.driver.back()
# 选择元素进行断言
result = self.driver.find_element(MobileBy.XPATH,"//*[@resource-id='android:id/text1'][1]")
print(result.text)
# 断言
assert result.text == "Access'ibility"
5.capability 配置参数解析
Capability 简介
功能:配置 Appium 会话,告诉 Appium 服务器需要自动化的平台的应用程序
形式:键值对的集合,键对应设置的名称,值对应设置的值
主要分为三部分
Session
Desired Capabilities
建立会话公共部分参数配置
键 | 描述 | 值 |
---|---|---|
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 |
noReset |
在当前 session 下不会重置应用的状态。默认值为 false |
true , false |
udid |
连接的真实设备的唯一设备编号 (Unique device identifier) | 例如 1ae203187fc012g |
Android 部分特有参数配置
键 | 描述 | 值 |
---|---|---|
appActivity |
Activity 的名字是指从你的包中所要启动的 Android acticity。他通常需要再前面添加. (例如 使用 .MainActivity 代替 MainActivity ) |
MainActivity , .Settings |
appPackage |
运行的 Android 应用的包名 | com.example.android.myApp , com.android.settings |
appWaitActivity |
用于等待启动的 Android Activity 名称 | SplashActivity |
unicodeKeyboard |
启用 Unicode 输入,默认为 false | true or false |
resetKeyboard |
true or false |
|
dontStopAppOnReset |
首次启动的时候,不停止 app | true or false |
skipDeviceInitialization |
跳过安装,权限设置等操作 | true or false |
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 |
Desire capability 参数示例
{
"platformName": "android",
"deviceName": "emulator-5554",
"appPackage": "io.appium.android.apis",
"appActivity": ".ApiDemos"
}
配置优化
{
"noReset": "true", // 不清空缓存信息
"dontStopAppOnReset": "true", // 首次启动的时候,不停止app
"skipDeviceInitialization": "true", // 跳过安装,权限设置等操作
"unicodeKeyBoard": "true" // 输入中文
}
6.app自动化控制
启动
webdriver.remote("url",desirecapability)
launch_app()
将应用启动起来数据清理
清空输入框内容
clear()
关闭
退出app
quit()
# 导入 pip install appium-python-client
from time import sleep
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
class TestAppDemo:
def setup(self):
# 创建一个字典,desirecapbility
caps = {}
caps["platformName"] = "Android"
# Android 包名和页面名,获取命令:
# mac/linux: adb logcat ActivityManager:I | grep "cmp"
# windows: adb logcat ActivityManager:I | findstr "cmp"
caps["appPackage"] = "io.appium.android.apis"
caps["appActivity"] = ".ApiDemos"
caps["deviceName"] = "127.0.0.1:7555 device"
caps["noReset"] = "true"
# 创建driver ,与appium server建立连接,返回一个 session
# driver 变成self.driver 由局部变量变成实例变量,就可以在其它的方法中引用这个实例变量了
self.driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
self.driver.implicitly_wait(5)
def teardown(self):
# 回收session
self.driver.quit()
def test_input(self):
# el1 = self.driver.find_element_by_accessibility_id("OS")
el1 = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "OS")
el1.click()
# el2 = self.driver.find_element_by_accessibility_id("Morse Code")
el2 = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Morse Code")
el2.click()
# el3 = self.driver.find_element_by_id("io.appium.android.apis:id/text")
el3 = self.driver.find_element(AppiumBy.ID, "io.appium.android.apis:id/text")
# 清除原有的内容
el3.clear()
el3.send_keys("baidu.com")
el3.clear()
# 手动制造关闭应用
sleep(5)
# 启动应用, 热启动,会进入到app 的首页
self.driver.launch_app()
result = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Accessibility").text
# 断言
assert result == "Accessibility"
7.常见控件定位方法
android 基础知识
Android 是通过容器的布局属性来管理子控件的位置关系,布局关系就是把界面上的所有的空间,根据他们的间距的大小,摆放在正确的位置
Android 七大布局
Android 四大组件
常用的控件
布局
嵌套布局
ios 基础知识
布局
注意
元素定位
概念:元素定位的含义就是定位控件
注意:同一脚本同时支持 android/iOS 两个系统的前提是: 元素属性(id,aid,xpath 等)一致
控件基础知识
dom:Document Object Model 文档对象模型
dom 应用:用于表示界面的控件层级,界面的结构化描述
xpath:xml 路径语言,用于 xml 中的节点定位
app dom 结构解析
示例
node
attribute
iOS 与 Android dom 结构的区别
定位方法
测试步骤三要素
定位方式:
常见的控件定位方法
App 定位方式
定位策略 | 描述 |
---|---|
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 的路径(不推荐,存在性能问题) |
App 定位方式进阶
定位策略 | 描述 |
---|---|
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 框架查找元素 |
选择定位器通用原则
元素定位的写法
# 返回单个元素 WebElement
driver.find_element(AppiumBy.xxx, "xxx属性值")
# 返回元素列表 [WebElement, WebElement, WebElement...]
driver.find_elements(AppiumBy.xxx, "xxx属性值")
元素定位的写法
driver.find_element(AppiumBy.ID, "ID属性值")
driver.find_element(AppiumBy.XPATH, "xpath表达式")
driver.find_element(AppiumBy.CLASS_NAME, "CLASS属性值")
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "ACCESSIBILITY_ID表达式")
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, "android uiautomator 表达式")
driver.find_element(AppiumBy.IOS_UIAUTOMATION, "ios uiautomation 表达式")
driver.find_element(AppiumBy.ANDROID_VIEWTAG, "ESPRESSO viewtag 表达式")
driver.find_element(AppiumBy.ANDROID_DATA_MATCHER, "ESPRESSO data matcher 表达式")
driver.find_element(AppiumBy.IMAGE, "IMAGE图片")
ID 定位
find_element(AppiumBy.ID, "ID属性值")
ACCESSIBILITY_ID 定位
find_element(AppiumBy.ACCESSIBILITY_ID, "ACCESSIBILITY_ID属性值")
XPath 定位
表达式 | 描述 |
---|---|
/ | 从根节点选取(取子节点)。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置(取子孙节点)。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
XPath 单属性定位
//*[@属性名='属性值']
XPath 多属性定位
//*[@属性名='属性值' and @属性名='属性值' ]
实战练习
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
class TestLocation:
def setup(self):
caps = {}
caps["platformName"] = "Android"
caps["appium:appPackage"] = "io.appium.android.apis"
caps["appium:appActivity"] = ".ApiDemos"
caps["appium:deviceName"] = "127.0.0.1:7555"
caps["dontStopAppOnReset"] = "true"
caps["noReset"] = "true"
self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
self.driver.implicitly_wait(5)
def teardown(self):
self.driver.quit()
def test_id(self):
"""通过 ID 进行元素定位"""
print(self.driver.find_element(AppiumBy.ID, "android:id/text1"))
def test_aid(self):
"""通过 ACCESSIBILITY_ID 进行元素定位"""
print(self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "App"))
def test_xpath(self):
"""通过 XPATH 进行元素定位"""
print(self.driver.find_element(AppiumBy.XPATH, "//*[@text='App']"))
def test_xpath1(self):
"""通过 XPATH 进行元素定位"""
print(self.driver.find_element(AppiumBy.XPATH, "//*[@text='App' and @resource-id='android:id/text1']"))
原生定位
Android 原生定位
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")'))
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
class TestLocation:
def setup(self):
caps = {}
caps["platformName"] = "Android"
caps["appium:appPackage"] = "io.appium.android.apis"
caps["appium:appActivity"] = ".ApiDemos"
caps["appium:deviceName"] = "127.0.0.1:7555"
caps["dontStopAppOnReset"] = "true"
caps["noReset"] = "true"
self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
self.driver.implicitly_wait(5)
def teardown(self):
self.driver.quit()
def test_id(self):
"""通过 ID 进行元素定位"""
print(self.driver.find_element(AppiumBy.ID, "android:id/text1"))
def test_aid(self):
"""通过 ACCESSIBILITY_ID 进行元素定位"""
print(self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "App"))
def test_xpath(self):
"""通过 XPATH 进行元素定位"""
print(self.driver.find_element(AppiumBy.XPATH, "//*[@text='App']"))
def test_xpath1(self):
"""通过 XPATH 进行元素定位"""
print(self.driver.find_element(AppiumBy.XPATH, "//*[@text='App' and @resource-id='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")'))
Android 原生定位-组合定位
driver.find_element_by_android_uiautomator('\
new UiSelector().resourceId("com.xueqiu.android:id/tab_name").\
text("我的")')
Android 原生定位-模糊匹配
# 模糊匹配
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)
Android 原生定位-层级定位
fromParent
childSelector
, 可以传入 resourceId() , description() 等方法# 查找目标元素Text,先找App ,fromParent() 方法可以查找兄弟结点
new UiSelector().text("App").fromParent(text("Text"))
# 根据父结点查找子结点/ 子孙结点
new UiSelector().className("android.widget.ListView").childSelector(text("Text"))
滑动查找元素
new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("查找的元素文本").instance(0))
总结
8.强制等待与隐式等待
为什么要添加等待
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/code").click()
driver.quit()
直接等待
time.sleep(3)
from appium import webdriver
import time
from appium.webdriver.common.appiumby import AppiumBy
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
time.sleep(3)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
time.sleep(3)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/code").click()
driver.quit()
隐式等待
显式等待基本使用(初级)
WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件)
from appium import webdriver
import time
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
class TestSleep:
def test_no_wait(self):
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/code").click()
driver.quit()
def test_force_wait(self):
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
time.sleep(3)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
time.sleep(3)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/code").click()
driver.quit()
def test_implicit_wait(self):
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
driver.implicitly_wait(15)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/code").click()
driver.quit()
def test_explicit_wait(self):
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.xueqiu.android'
desired_caps['appActivity'] = 'com.xueqiu.android.common.MainActivity'
desired_caps['noReset'] = "true"
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
driver.implicitly_wait(15)
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/tv_search").click()
driver.find_element(AppiumBy.ID, \
"com.xueqiu.android:id/search_input_text").send_keys("alibaba")
WebDriverWait(driver, 10, 0.5).until(
expected_conditions.element_to_be_clickable((AppiumBy.ID, 'com.xueqiu.android:id/code')))
driver.find_element(AppiumBy.ID, "com.xueqiu.android:id/code").click()
driver.quit()
类型 | 使用方式 | 原理 | 适用场景 |
---|---|---|---|
直接等待 | time.sleep(等待时间)) |
强制线程等待 | 调试代码,临时性添加 |
隐式等待 | driver.implicitly_wait(等待时间) |
在时间范围内,轮询查找元素 | 解决找不到元素问题,无法解决交互问题 |
显式等待 | WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件) |
设定特定的等待条件,轮询操作 | 解决特定条件下的等待问题,比如点击等交互性行为 |
9.常见控件交互方法
元素的常用方法
点击方法 element.click()
输入操作 element.send_keys('appium')
设置元素的值 element.set_value('appium')
清除操作 element.clear()
是否可见 element.is_displayed()
返回 True/False
是否可用 element.is_enabled()
返回 True/False
是否被选中 element.is_selected()
返回 True/False
获取属性值 get_attribute(name)
get_attribute()
方法能获取的属性,元素的属性几乎都能获取到,属性名称和 uiautomatorviewer 里面的一致
源码地址: https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/GetElementAttribute.java
get_attribute() 可以获取的属性
元素常用属性
{'y': 19,'x: 498}
{'width':500,'height':22)
代码示例:
from time import sleep
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
class TestControl:
def setup(self):
# 创建一个字典,desirecapbility
caps = {}
caps["platformName"] = "Android"
caps["deviceName"] = "127.0.0.1:7555 device"
caps["noReset"] = "true"
# 创建driver ,与appium server建立连接,返回一个 session
# driver 变成self.driver 由局部变量变成实例变量,就可以在其它的方法中引用这个实例变量了
self.driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
self.driver.implicitly_wait(5)
def teardown(self):
# 回收session
self.driver.quit()
def test_seeking(self):
"""
打开 demo.apk
1. 点击 Animation 进入下个页面
2. 点击 Seeking 进入下个页面
3. 查看【RUN】按钮是否显示/是否可点击
4. 查看【滑动条】是否显示/是否可用/是否可点击
5. 获取【滑动条】长度
6. 点击【滑动条】中心位置
:return:
"""
self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Animation").click()
self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Seeking").click()
# 3. 查看【RUN】按钮是否显示/是否可点击
run_element = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Run")
run_is_displayed = run_element.is_displayed()
run_is_clickable = run_element.get_attribute("clickable")
print(f"【run】按钮是否可见:{run_is_displayed},是否可点击:{run_is_clickable}")
# 4. 查看【滑动条】是否显示/是否可用/是否可点击
seekbar_element = self.driver.find_element(AppiumBy.ID, "io.appium.android.apis:id/seekBar")
seekbar_displayed = seekbar_element.is_displayed()
seekbar_enabled = seekbar_element.is_enabled()
seekbar_clickable = seekbar_element.get_attribute("clickable")
print(f"seekbar 滑动条 是否可见:{seekbar_displayed},"
f"是否可用:{seekbar_enabled},"
f"是否可点击:{seekbar_clickable}")
# 5.获取【滑动条】长度
seekbar_size = seekbar_element.size
width = seekbar_size.get("width")
height = seekbar_size.get("height")
print(f"seekbar 的长度:{width} seekbar 的高度:{height}")
seekbar_location = seekbar_element.location
x = seekbar_location.get("x")
y = seekbar_location.get("y")
# 6.点击【滑动条】中心位置
seekbar_centerx = x + width / 2
seekbar_centery = y
self.driver.tap([(seekbar_centerx, seekbar_centery)])
sleep(5)
10.自动化测试定位策略
App 定位方式
定位策略 | 描述 |
---|---|
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 的路径(不推荐,存在性能问题) |
App 定位方式进阶
定位策略 | 描述 |
---|---|
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 框架查找元素 |
选择定位器通用原则
元素定位不到
原因 | 解决方案 |
---|---|
定位不正确 | 在定位工具中先测试定位表达式是否正确 |
存在动态 ID | 定位方式使用 css 或者 xpath 的相对定位 |
页面还没有加载完成 | 添加死等验证,使用显示等待或隐式等待进行优化 |
页面有 iframe | 切换到 iframe 后定位 |
页面切换 window | 切换到对应窗口后定位 |
要定位元素为隐藏元素 | 使用 js 操作该元素 |
混合定位的应用场景
使用等待机制的场景
App toast 提示框定位
下拉框/日期控件定位
场景:
标签组合的下拉框无法定位
标签组合的日期控件无法定位解决:
文件上传定位
11.雪球app搜索功能点自动化测试实战
代码示例:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
class TestSearch:
def setup(self):
# 创建一个字典,desirecapbility
caps = {}
caps["platformName"] = "Android"
caps["appPackage"] = "com.xueqiu.android"
caps["appActivity"] = ".view.WelcomeActivityAlias"
caps["deviceName"] = "emulator-5554"
caps["noReset"] = "true"
self.driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
# 隐式等待
self.driver.implicitly_wait(20)
def teardown(self):
# 关闭应用
self.driver.quit()
def test_search(self):
"""
1. 判断搜索框的是否可用,并查看搜索框 name 属性值,并获取搜索框坐标,以及它的宽高
2. 点击搜索框
3. 向搜索框输入:alibaba
4. 判断【阿里巴巴】是否可见
如果可见,打印“搜索成功”
如果不可见,打印“搜索失败
:return:
"""
# 1. 判断搜索框的是否可用,并查看搜索框 name 属性值,并获取搜索框坐标,以及它的宽高
search_key = "alibaba"
searchbox_ele = self.driver.find_element(AppiumBy.ID, "com.xueqiu.android:id/home_search")
# 先判断一下搜索框是否可用
if searchbox_ele.is_enabled():
searchbox_text = searchbox_ele.text
searchbox_location = searchbox_ele.location
searchbox_size = searchbox_ele.size
print(f"首页搜索框的 text:{searchbox_text}")
print(f"首页搜索框的 location坐标为:{searchbox_location}")
print(f"首页搜索框的 size 宽高:{searchbox_size}")
# 2. 点击搜索框
searchbox_ele.click()
# 3. 向搜索框输入:alibaba
self.driver.find_element(AppiumBy.ID,
"com.xueqiu.android:id/search_input_text").send_keys(search_key)
# 4. 判断【阿里巴巴】是否可见
# 如果可见,打印“搜索成功”
# 如果不可见,打印“搜索失败
#
alibaba_element = self.driver.find_element(AppiumBy.XPATH, "//*[@text='阿里巴巴']")
result = alibaba_element.is_displayed()
# print(result)
if result == True:
print("搜索成功")
else:
print("搜索失败")
assert result == True
else:
print("搜索框不可用")
assert False