高级定位技巧-css 定位与原生定位
学们,大家好!今天我们要聊的是 App 自动化测试中的高级定位技巧,主要围绕 CSS 定位和原生定位展开。定位元素是自动化测试的关键,学会不同的定位方式,可以让我们的脚本更加稳定、高效。接下来,我们一步步深入学习!
简介
在自动化测试中,元素定位是核心,找不到元素,后续的操作就无从谈起。不同的应用类型,适合不同的定位方法。我们需要根据实际情况,选择合适的方式,以保证脚本的稳定性和执行效率。
App 的种类
- 原生
App(Native app)
: 基于Android
或ios
平台官方的语言,工具进行开发 Web App
:通过web
语言进行开发,依托浏览器运行。- 混合
App(Hybrid App)
:同时使用了原生技术和HTML5
技术。 - 总结:原生速度最优,
web app
成本低,支持广,混合APP(Hybrid App)
开发周期短,功能更新快
我们先来看看 App 的分类,主要有三种:原生 App(Native App)是用 Android 或 iOS 官方的开发语言,比如 Java、Kotlin、Swift 进行开发,运行速度最快,能充分调用系统资源。Web App 本质上是一个网页应用,在手机浏览器里运行,比如移动版淘宝、微博网页版,开发成本低,适配性好。混合 App(Hybrid App)则是结合了原生和 Web 技术,比如支付宝、小红书,这类 App 既能调用系统功能,又能用 H5 进行页面渲染,开发和更新速度更快。总结一下,原生 App 性能最好,Web App 兼容性强但受限于浏览器,混合 App 是一个折中方案,适用于功能更新频繁的场景。
原生定位
Android 系统原生定位
appium
在 Android
端调用其底层的 UIAutomator2
自动化测试框架去进行元素定位,借助UIAutomator API(UISelector类)
来搜索特定元素。
除了之前介绍的方式,App 还支持系统原生的定位方式。在 Android 端,Appium 主要是调用 UIAutomator2 这个自动化测试框架来进行元素定位。它使用 UISelector 这个类来查找特定的元素,比如 ID、文本、类名等。
IOS 系统原生定位
对于 9.2
及以下版本的 IOS
系统,苹果唯一的自动化技术为 UIAutomation
,它运行在Instruments
中。
对于 9.3
及以上版本的IOS
系统,苹果已经淘汰了 UIAutomation
工具,转而使用XCUITest
自动化技术,而对于 appium
而言,1.6 版本才开始支持 XCUITest
。
IOS 系统原生定位的自动化技术经历了一些变化:iOS 9.2 及以下 使用 UIAutomation,它运行在 Instruments 中(苹果的测试工具)。iOS 9.3 及以上,苹果淘汰了 UIAutomation,引入了 XCUITest,而 Appium 1.6 版本开始支持 XCUITest。所以说对于 iOS 自动化测试,XCUITest 是目前的主流工具,性能更好,也更加稳定。
CSS Selector 定位
CSS
选择器是由 Web
开发领域的标准化组织 W3C(World Wide Web Consortium)
制定和实现的。appium
上使用的 CSS Selector
和 web
上使用的大致相同,但是在实际使用上有些许差异。
CSS Selector 是前端开发里常用的选择器,由 W3C 组织制定,主要用于网页元素定位。在 Appium 里,我们也可以用 CSS Selector 来定位 WebView 或 Hybrid App 的元素。不过要注意,Appium 里的 CSS Selector 和 Web 端有些许差异,所以在使用时要特别留意。
应用场景
- 原生定位使用场景:针对一些重视性能的场景时,建议使用原生定位,因为原生定位一般使用的是移动端系统的内嵌框架进行操作,其余定位方法其实都是对原生定位语法的调用。
- CSS Selector 定位:针对复杂结构但是元素属性相对独立的应用,可以使用
CSS Selector
定位。CSS Selector
着重于通过元素的属性进行定位。
既然有这么多种定位方式,我们该怎么选择合适的呢?来看几个常见场景:如果是 Native App,推荐使用原生定位方式,比如 UIAutomator2(Android)或 XCUITest(iOS)。它们直接调用系统框架,性能最好,定位最精准。如果你的 App 是 Hybrid App 或 WebView,并且页面结构复杂,但元素属性明确,那么 CSS Selector 会是个不错的选择。选对定位方式,可以让我们的自动化测试更加稳定高效!
Android 原生元素定位
接下来,咱们先了解一下 Android 原生元素定位的方式。这里给大家提供了官方文档的地址。如果你想深入学习 Android 的原生元素定位,可以参考官方文档。我们会挑选一些最常用的给大家做介绍。
单属性定位
-
格式
'new UiSelector().属性名("<属性值>")'
。 -
比如:
'new UiSelector().resourceId("android:id/text1")'
。
注意:外面是单引号,里面是双引号,顺序不能变。
在 Android 里,我们可以通过 UiSelector 类的单属性来定位元素,格式如下。举个例子,如果我们想要定位一个 resourceId 为 android:id/text1 的元素,可以这样来写。这里需要注意,代码外层要用单引号,里面的字符串用双引号,这个顺序不能弄反,否则 Appium 会报错。
Python 示例:
# ApiDemos.apk Accessibility 控件
# ID定位
ass_id = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("android:id/text1")'
)
print('ass_id', ass_id)
# 文本定位
ass_text = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().text("Accessibility")'
)
# className 定位
ass_classname = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.TextView")'
)
先来看一个 python 中的示例。这里使用三种定位方式。通过 ID 定位是通过 resourceId("android:id/text1") 来找到指定的元素,并返回给 ass_id 变量。通过文本定位是通过 text("Accessibility") 来定位元素,即找到文本是 "Accessibility" 的 UI 控件。通过 className 定位是通过 className("android.widget.TextView") 来定位 所有 TextView 类型的控件。
Java 示例:
// ApiDemos.apk Accessibility控件
// ID定位
WebElement assId = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().resourceId('android:id/text1')"));
System.out.println("ass_id: " + assId);
// 文本定位
WebElement assText = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().text('Accessibility')"));
// className 定位
WebElement assClassname = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().className('android.widget.TextView')"));
Java 代码和 Python 类似,只是语法稍有不同。
组合定位
多个属性同时确定元素的(多个属性任意组合 ,不限长度)。
Python 示例:
# App 控件
ass_mul = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("android:id/text1").text("App")'
)
我们可以同时使用多个属性来精确定位元素,例如这里的 resourceId 和 text 组合在一起,确保找到的是 ID 为 android:id/text1,且文本为 "App" 的控件。
Java 示例:
// App 控件
WebElement assMul = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().resourceId('android:id/text1').text('App')"));
Java 中也是类似的写法。
模糊匹配
- 文字包含。
- 文字以 x 开头。
- 文字正则匹配。
如果我们不确定文本的完整内容,可以使用模糊匹配。比如可以匹配文字中包含某些值,或者文字义某些值开头,再或者直接用正则匹配的方式。
Python 示例:
# 模糊匹配
# contains
ass_text_contains = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textContains("ssi")'
)
print(ass_text_contains.text)
# start with
ass_text_contains_start = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textStartsWith("Ani")'
)
print(ass_text_contains_start.text)
# match
ass_match_contains = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textMatches("^Pre.*")'
)
print(ass_match_contains.text)
来看一下 python 中具体的例子。textContains("ssi"):查找包含"ssi" 的文本。textStartsWith("Ani"):查找以"Ani" 开头的文本。textMatches("^Pre.*"):使用正则表达式查找文本。这些方式可以让定位更加灵活,尤其适用于一些动态变化的文本内容。
Java 示例:
WebElement assTextContains = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().textContains('ssi')"));
System.out.println(assTextContains.getText());
// start with
WebElement assTextContainsStart = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().textStartsWith('Ani')"));
System.out.println(assTextContainsStart.getText());
// match
WebElement assMatchContains = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().textMatches('^Pre.*')"));
System.out.println(assMatchContains.getText());
Java 中也是类似的写法。
层级定位
- 兄弟元素定位:
fromParent
。 - 父子结点定位:
childSelector
, 可以传入 resourceId() , description() 等方法。
有时候,我们需要查找父子元素或者兄弟元素:fromParent(),用于从一个元素的父级找到另一个子级元素。childSelector(),用于从父级查找子级元素。
Python 示例:
# 兄弟元素
ass_bro = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().text("App").fromParent(text("Text"))'
)
print('ass_bro', ass_bro)
# 父子元素
ass_fa = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().\
className("android.widget.ListView").\
childSelector(text("Text"))'
)
print('ass_fa', ass_fa)
来看个 python 中的例子。找兄弟元素的时候,是在文本是 Text 的元素的父元素中找文本为 App 的元素。而父子元素的例子中,表示从 android.widget.ListView 元素中查找文本为 Text 的元素。
Java 示例:
// 兄弟元素
WebElement assBro = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().text('App').fromParent(text('Text'))"));
System.out.println("assBro" + assBro);
// 父子元素
WebElement assFa = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().className('android.widget.ListView').childSelector(text('Text'))'"));
System.out.println("assFa" + assFa);
Java 中也是类似的写法。
滑动查找元素
在 Appium 中进行 Android 原生定位时,滑动查找元素是一种常见的操作,尤其是在面对长列表或需要查找位于屏幕之外的元素时。
"new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text('查找的元素文本').instance(0))"
如果元素不在当前屏幕,需要滑动查找:这段代码的作用是在可滚动区域内找到指定文本的元素。如果元素在屏幕外,Appium 会自动滚动页面,直到找到该元素。
CSS Selector 定位
使用 CSS 定位时,定位方式和 Web 自动化类似,语法如下:
查找单个元素
- Python:
driver.find_element(AppiumBy.CSS_SELECTOR, selector)
- Java:
WebElement element = driver.findElement(AppiumBy.cssSelector(selector));
CSS 选择器是一种强大的定位方式,在 Web 自动化测试中被广泛使用。而在 Appium 中,我们同样可以使用 CSS 选择器来查找元素。这里的语法和 Selenium 的类似,只是在 Appium 中需要使用 AppiumBy.CSS_SELECTOR。例如,在 Python 中,我们可以用 driver.find_element(AppiumBy.CSS_SELECTOR, selector) 来查找单个元素,而 Java 则是 driver.findElement(AppiumBy.cssSelector(selector))。
查找多个元素
- Python:
driver.find_elements(AppiumBy.CSS_SELECTOR, selector)
- Java:
List<WebElement> elements = driver.findElements(AppiumBy.cssSelector(selector));
有时候,我们可能需要找到一组相同类型的元素,比如多个按钮或者列表项。这时候就需要用 find_elements 方法,而不是 find_element。这里给了大家 python 和 java 的示例。总之,需要使用定位方式中的 CSS_SELECTOR。
CSS 常见语法
定位方式 | 语法格式 | 描述 |
---|---|---|
标签名 | selector = "input" |
表示查找所有的 input 元素 |
id | selector = "#elementId" |
表示查找 id 为"elementId"的元素。"#elementId"中的"#"表示 id 属性。 |
class | selector = ".elementClass" |
表示查找 class 属性为"elementClass"的元素。".elementClass"中的"."表示 class 属性。 |
属性定位 | selector = "input[name='username']" |
表示查找 name 属性为"username"的 input 元素。可以根据元素的其他属性进行定位,如"value"、"type"等。 |
层级关系 | selector = "div > input" |
表示查找 div 下所有的 input 元素。 |
文本内容 | selector = "button:contains('Submit')" |
表示查找文本内容包含"Submit"的所有 button 元素。:contains('Submit') 表示匹配包含指定文本的元素。 |
在 CSS 选择器的语法中,我们可以通过不同的方式来定位元素,比如:标签名:input 选择所有 input 标签。id 选择器:#elementId 选择 id 为 elementId 的元素。class 选择器:.elementClass 选择 class 为 elementClass 的元素。属性选择器:input[name='username'] 选择 name="username" 的输入框。层级选择器:div > input 选择 div 下的 input 元素。文本匹配:button:contains('Submit') 选择文本包含 "Submit" 的 button 元素。在 Appium 中使用 CSS 选择器时,需要注意移动端的 HTML 结构并不是标准 Web 页面,因此有时可能需要结合其他定位方式。
CSS Selector 定位原理分析
CSS Selector 的定位表达式在传入 appium 服务器时通常会经过一系列解析步骤,最终被转换为原生定位的形式,以便在移动应用中准确地定位和操作元素。大致的解析过程如下所示:
- 解析 CSS 选择器:Appium 解析器首先会分析和解释 CSS 选择器的语法和结构。
- 映射到原生定位:解析器将 CSS 选择器映射到移动应用测试框架所支持的原生定位方式。
Appium 处理 CSS 选择器的方式其实是将其转换为移动应用所支持的原生定位方式。解析 CSS 选择器:首先 Appium 服务器会解析传入的 CSS 选择器语法。转换为原生定位:Appium 会将 CSS 选择器转换为 Android 或 iOS 支持的原生查找方式。由于移动应用不像 Web 页面那样完全支持 CSS 选择器,因此在实际应用中,通常推荐使用 XPath 或 UIAutomator 进行元素定位。
代码:
driver.find_element(AppiumBy.CSS_SELECTOR,\
"#com\.xueqiu\.android\:id\/tv_search")
解析前:
{"using":"css selector",\
"value":"#com\\.xueqiu\\.android\\:id\\/tv_search"}
解析后:
{"strategy":"-android uiautomator",\
"selector":"new UiSelector().resourceId
(\"com.xueqiu.android:id/tv_search\")",...}
由此可以看出,由于 CSS 选择器是针对 Web 开发涉及的,有时在移动应用中的解析过程可能不如直接使用移动应用测试框架提供的原生定位方式那样高效。因此,根据实际需求和应用特性,大多推荐使用 XPATH 或者原生定位的方式去对元素进行操作。
在实际的 Appium 运行过程中,CSS 选择器的解析大致是这样的。Appium 会将 CSS 选择器转换成 UiSelector() 语法,并使用 resourceId 进行定位。因此,在 Android 上,使用 UIAutomator 可能比 CSS 选择器更加高效。
示例
场景:
- 打开【雪球】应用首页
- 点击搜索框
- 向搜索框输入: alibaba
- 判断【阿里巴巴】可见
假设我们要在雪球 App 中完成以下操作:第一步打开雪球首页,第二步点击搜索框,第三步在搜索框中输入 alibaba,最后检查页面上是否出现阿里巴巴这个搜索结果。这个操作是一个典型的 UI 自动化测试场景,我们可以用 Python 或 Java 来实现。
代码实现
- 使用 setup_method 函数创建 driver 建立连接并打开【雪球】应用首页。
- 添加 teardown_method 函数关闭应用。
Python 示例:
class TestXueqiu:
def setup_method(self):
# 设置 cpability
caps = {
# 设置 app 安装的平台(Android,iOS)
"platformName": "Android",
# 设置 appium 驱动
"appium:automationName": "uiautomator2",
# 设置设备名称
"appium:deviceName": "emulator-5554",
# 设置被测 app 的包名
"appium:appPackage": "com.xueqiu.android",
# 设置被测 app 启动页面的 Activity
"appium:appActivity": ".view.WelcomeActivityAlias",
# 不清空缓存信息,保存登录信息
"appium:noReset": True,
# 强制app重启,整个测试运行之前重启 app
"appium:forceAppLaunch": True
}
self.driver = webdriver.Remote(
"http://localhost:4723",
options=UiAutomator2Options().load_capabilities(cpas)
)
self.driver.implicitly_wait(15)
def teardown_method(self):
self.driver.quit()
在 Python 中,我们使用 setup_method 和 teardown_method 来管理 Appium 连接。这里的 setup_method 负责启动 App 并建立连接,而 teardown_method 则会在测试完成后关闭 App。
Java 示例:
public class TestXueqiu {
private static AndroidDriver driver;
@BeforeAll
public static void setUp() throws MalformedURLException {
// 初始化capability
DesiredCapabilities caps = new DesiredCapabilities();
// 设置 app 安装的平台(Android、iOS)
caps.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
// 设置 appium 驱动
caps.setCapability(MobileCapabilityType.AUTOMATION_NAME, "UiAutomator2");
// 设置设备名称
caps.setCapability(MobileCapabilityType.DEVICE_NAME, "emulator-5554");
// 设置 app 的包名
caps.setCapability("appPackage", "com.xueqiu.android");
// 设置 app 的启动页
caps.setCapability("appActivity", ".view.WelcomeActivityAlias");
// 设置 app 不清空缓存
caps.setCapability("appium:noReset", true);
// 设置 app 不重启
caps.setCapability("appium:shouldTerminateApp", true);
URL remoteUrl = new URL("http://127.0.0.1:4723");
driver = new AndroidDriver(remoteUrl, caps);
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
}
@AfterAll
public static void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
在 Java 中,我们可以使用 setUp 方法初始化 AndroidDriver。这里的 setUp 方法会初始化 AndroidDriver,并设置 appPackage 和 appActivity 以启动应用。
- 使用原生定位判断【阿里巴巴】是否可见:
Python 示例:
class TestXueqiu:
def test_search(self):
'''
使用原生定位
'''
# 点击搜索框
self.driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.xueqiu.android:id/home_search")'
).click()
# 输入 阿里巴巴
self.driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.xueqiu.android:id/search_input_text"'
).send_keys("阿里巴巴")
# 获取当前页面包含文本 阿里巴巴 的元素
alibaba_element = self.driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().text("阿里巴巴")'
)
# 判断属性为 true
result = alibaba_element.get_attribute("displayed")
# 断言最终结果正确
assert result == 'true'
在 Appium 中,我们可以用 UIAutomator 定位元素。通过 resourceId 和 text 定位元素,并断言 "阿里巴巴" 是否可见。
Java 示例:
@Test
public void testSearch() {
// 使用原生定位
// 点击搜索框
WebElement searchButtonEle = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().resourceId('com.xueqiu.android:id/home_search')"));
searchButtonEle.click();
// 输入阿里巴巴
WebElement searchInputEle = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().resourceId('com.xueqiu.android:id/search_input_text'"));
searchInputEle.sendKeys("阿里巴巴");
// 获取当前页面包含文本 阿里巴巴 的元素
WebElement alibabaElement = driver.findElement(AppiumBy.androidUIAutomator("new UiSelector().text('阿里巴巴')"));
// 判断属性为 true
String result = alibabaElement.getAttribute("displayed");
// 判断【阿里巴巴】可见
assert result.equals("true");
}
在 Java 代码中,我们可以使用类似的方法。这段代码同样使用 UIAutomator 定位元素,并检查 "阿里巴巴" 是否可见。
- 同样的步骤,使用 CSS Selector 实现:
Python 示例:
class TestXueqiu:
def test_search_withcss(self):
'''
使用 css 定位
'''
# 点击搜索框
self.driver.find_element(
AppiumBy.CSS_SELECTOR,
"#com\.xueqiu\.android\:id\/home_search"
).click()
# 向搜索框输入:阿里巴巴
self.driver.find_element(
AppiumBy.CSS_SELECTOR, "#com\.xueqiu\.android\:id\/search_input_text"
).send_keys("阿里巴巴")
# 获取当前页面文本包含 阿里巴巴 的元素
alibaba_element = self.driver.find_element(
AppiumBy.CSS_SELECTOR, "*[text='阿里巴巴']"
)
# 获取可见属性
result = alibaba_element.get_attribute("displayed")
## 判断【阿里巴巴】可见
assert result == "true"
同样的步骤,使用 CSS Selector 实现可以这样写。
Java 示例:
@Test
public void testSearch() {
// 使用 css 定位
// 点击搜索框
WebElement searchButtonEle = driver.findElement(AppiumBy.cssSelector("#com\\.xueqiu\\.android\\:id\\/home_search"));
searchButtonEle.click();
// 输入阿里巴巴
WebElement searchInputEle = driver.findElement(AppiumBy.cssSelector("#com\\.xueqiu\\.android\\:id\\/search_input_text"));
searchInputEle.sendKeys("阿里巴巴");
// 获取当前页面包含文本 阿里巴巴 的元素
WebElement alibabaElement = driver.findElement(AppiumBy.cssSelector("*[text='阿里巴巴']"));
// 判断属性为 true
String result = alibabaElement.getAttribute("displayed");
// 判断【阿里巴巴】可见
assert result.equals("true");
}
Java 也是类似的实现。
总结
- Android 原生元素定位
- iOS 原生元素定位
- CSS Selector 定位
最后我们来总结一下。在 Appium 中可以使用 CSS Selector 进行元素定位,但底层依然会转换为 原生定位 方式。原生定位(UIAutomator 或 XPath)通常比 CSS Selector 更高效。在实际测试中,应该根据性能和可维护性选择合适的定位方式。