本文为霍格沃兹测试开发学社学员学习笔记分享
原文链接:https://ceshiren.com/t/topic/25083
1)获取 app 的信息
2)配置待测应用
1)app 入口,两种方式获取:
* 通过 logcat 日志获取
Mac/Linux: adb logcat ActivityManager:I | grep “cmp"
Windows: adb logcat ActivityManager:I | findstr "cmp"
* 通过 aapt 获取
Mac/Linux: aapt dump badging wework.apk | grep launchable-activity
Windows: aapt dump badging wework.apk | findstr launchable-activity
2)启动应用命令 adb shell am start -W -n / -S
1)platformName:平台,Android/iOS
2)deviceName:设备名
3)appPackage:应用的包名
4)appActivity:应用的页面名 Activity
5)noReset: 防止清空缓存信息
1)SelectElements:选中元素,查看层级和属性
2)Swipe By Coordinates:通过坐标点滑动
3)Tap By Coordinates:通过坐标点点击
4)Back:返回
5)Refresh Source & Screenshot:刷新页面
6)StartRecording:开始录制脚本
7)Search for element:搜索元素
8)Copy XML Source to Clipboard:复制 xml 结构
9)Quit Session & Close Inspector:退出当前 Session
1)打开 API Demo 应用
2)点击 OS,进入下个界面
3)点击【Morse Code】
4)输入内容【ceshiren.com】
5)返回上一个页面
6)返回上一个页面
7)关闭应用
from appium import webdriver
# 创建一个字典,desirecapbility
caps = {}
# Android 包名和页面名,获取命令:
# mac/linux: adb logcat ActivityManager:I | grep "cmp"
# windows: adb logcat ActivityManager:I | findstr "cmp"
caps["platformName"] = "Android"
caps["appPackage"] = "io.appium.android.apis"
caps["appActivity"] = ".ApiDemos"
caps["deviceName"] = "127.0.0.1:6155"
caps["ensureWebviewsHavePages"] = True
# 创建driver ,与appium server建立连接,返回一个 session
driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
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()
el4 = driver.find_element_by_id("io.appium.android.apis:id/text")
el4.send_keys("ceshiren.com")
# 返回
driver.back()
driver.back()
driver.back()
# 回收session
driver.quit()
添加 capability 信息
初始化webdriver
,添加setup
和teardown
添加隐式等待和noReset
属性增强用例稳定性
添加断言
注意
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:6555"
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):
"""
1、打开 API demo apk
2、点击 OS 控件
3、点击 Morse Code 控件
4、在搜索框中输入 ceshiren.com
5、返回到第一页
6、断言
:return:
"""
# 点击OS控件
# el1 = self.driver.find_element_by_accessibility_id("OS")
el1 = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "OS")
el1.click()
# 点击 Morse Code 控件
# el2 = self.driver.find_element_by_accessibility_id("Morse Code")
el2 = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Morse Code")
el2.click()
# 输入`ceshiren.com`
# 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("ceshiren.com")
el3.clear()
# 返回
self.driver.back()
# 返回
self.driver.back()
# 返回第一页
self.driver.back()
# 选择元素进行断言
result = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Accessibility").text
# 断言
assert result == "Accessibility"
功能:配置 Appium 会话,告诉 Appium 服务器需要自动化的平台的应用程序
形式:键值对的集合,键对应设置的名称,值对应设置的值
主要分为三部分
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 |
键 | 描述 | 值 |
---|---|---|
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 |
键 | 描述 | 值 |
---|---|---|
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 |
键 | 描述 | 值 |
---|---|---|
bundleId | 被测应用的 bundle ID 。用于在真实设备中启动测试,也用于使用其他需要 bundle ID 的关键字启动测试。在使用 bundle ID 在真实设备上执行测试时,你可以不提供 app 关键字,但你必须提供 udid 。 | 例如 io.appium.TestApp |
autoAcceptAlerts | 当 iOS 的个人信息访问警告 (如 位置、联系人、图片) 出现时,自动选择接受( Accept )。默认值 false | true 或者 false |
showIOSLog | 是否在 appium 日志中显示从设备捕获的任何日志。默认 false | true or false |
{
"platformName": "android",
"deviceName": "emulator-5554",
"appPackage": "io.appium.android.apis",
"appActivity": ".ApiDemos"
}
{
"noReset": "true", // 不清空缓存信息
"dontStopAppOnReset": "true", // 首次启动的时候,不停止app
"skipDeviceInitialization": "true", // 跳过安装,权限设置等操作
"unicodeKeyBoard": "true" // 输入中文
}
webdriver.remote("url",desirecapability)
launch_app()
将应用启动起来# 方式一:
self.driver = webdriver.Remote\
("http://127.0.0.1:4723/wd/hub", desire_cap)
# 方式二:热启动,会进入到app的首页
self.driver.launch_app()
清空输入框内容
clear()
self.driver.find_element_by_accessibility_id('SomeAccessibilityID').clear()
退出app
quit()
self.driver.quit()
Android 是通过容器的布局属性来管理子控件的位置关系,布局关系就是把界面上的所有的空间,根据他们的间距的大小,摆放在正确的位置
Android 七大布局
布局
嵌套布局
布局
注意
dom:Document Object Model 文档对象模型
dom 应用:用于表示界面的控件层级,界面的结构化描述
xpath:xml 路径语言,用于 xml 中的节点定位
Anrdroid 应用的层级结构与 html 不一样,是一个定制的 xml
app source 类似于 dom ,表示 app 的层级,代表了界面里面所有的控件树的结构
每个控件都有它的属性(resourceid,xpath,aid),但是没有 css 属性
node
attribute
clickable
content-desc
resource-id
text
bounds
dom 属性和节点结构类似
名字和属性命名不同
测试步骤三要素
定位方式:
定位策略 | 描述 |
---|---|
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 框架查找元素 |
与研发约定的属性优先
身份属性 id
组合定位 xpath,css
其它定位
# 返回单个元素 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图片")
find_element(AppiumBy.ID, "ID属性值")
find_element(AppiumBy.ACCESSIBILITY_ID, "ACCESSIBILITY_ID属性值")
表达式 | 描述 |
---|---|
/ | 从根节点选取(取子节点)。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置(取子孙节点)。 |
. | 选取当前节点。 |
… | 选取当前节点的父节点。 |
@ | 选取属性。 |
//*[@属性名='属性值']
//*[@属性名='属性值' and @属性名='属性值' ]
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']"))
格式 '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")'))
driver.find_element_by_android_uiautomator('\
new UiSelector().resourceId("com.xueqiu.android:id/tab_name").\
text("我的")')
# 模糊匹配
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)
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))
time.sleep(3)
from appium import webdriver
import time
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()
#设置一个等待时间,轮询查找(默认0.5秒)元素是否出现,如果没出现就抛出异常
driver.implicitly_wait(3)
元素可以找到,使用点击等操作,出现报错
原因:
解决方案:使用显式等待
WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件)
WebDriverWait(driver, 10).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()
类型 | 使用方式 | 原理 | 适用场景 |
---|---|---|---|
直接等待 | time.sleep(等待时间)) | 强制线程等待 | 调试代码,临时性添加 |
隐式等待 | driver.implicitly_wait(等待时间) | 在时间范围内,轮询查找元素 | 解决找不到元素问题,无法解决交互问题 |
显式等待 | WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件) | 设定特定的等待条件,轮询操作 | 解决特定条件下的等待问题,比如点击等交互性行为 |
点击方法 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)
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()
# 查看【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}")
# 查看【滑动条】是否显示/是否可用/是否可点击
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}")
# 获取【滑动条】长度
seekbar_size = seekbar_element.size
width = seekbar_size.get("width")
height = seekbar_size.get("height")
print(f"seekbar 的长度:{width}")
seekbar_location = seekbar_element.location
x = seekbar_location.get("x")
y = seekbar_location.get("y")
# 点击【滑动条】中心位置
seekbar_centerx = x + width / 2
seekbar_centery = y
self.driver.tap([(seekbar_centerx, seekbar_centery)])
sleep(5)
定位策略 | 描述 |
---|---|
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 框架查找元素 |
定位策略 | 描述 |
---|---|
class name | 通过 class 属性定位元素 |
css selector | 通过匹配css selector 定位元素 |
id | 通过 id 属性匹配元素 |
name | 通过 name 属性定位元素 |
link text | 通过 text 标签中间的text文本定位元素 |
partial link text | 通过 text 标签中间的text文本的部分内容定位元素 |
tag name | 通过 tag 名称定位元素 |
xpath | 通过 xpath 表达式匹配元素 |
与研发约定的属性优先
身份属性 id,name(web 定位)
组合定位 xpath,css
其它定位
原因 | 解决方案 |
---|---|
定位不正确 | 在定位工具中先测试定位表达式是否正确 |
存在动态 ID | 定位方式使用 css 或者 xpath 的相对定位 |
页面还没有加载完成 | 添加死等验证,使用显示等待或隐式等待进行优化 |
页面有 iframe | 切换到 iframe 后定位 |
页面切换 window | 切换到对应窗口后定位 |
要定位元素为隐藏元素 | 使用 js 操作该元素 |
场景:
解决:
参考高级定位技巧章节(xpath,css)
场景
解决
场景
解决:
driver.switchTo().alert()
处理场景
解决:
场景:
标签组合的下拉框无法定位
标签组合的日期控件无法定位解决:
场景:
解决: