Appium Android自动化:从`findElementByName`到`UIAutomator`定位策略的迁移实践
在Appium的早期版本中,例如1.7.6之前的版本,使用findElementByName方法通过元素的name属性进行定位是一种常见的做法。然而,随着Appium版本的迭代,尤其是从Appium 1.7.6及更高版本开始,name作为定位策略在某些上下文中不再被官方支持,这会导致在执行测试脚本时抛出org.openqa.selenium.InvalidSelectorException异常。
以下是一个典型的错误栈追踪示例,表明了name选择器策略的无效性:
org.openqa.selenium.InvalidSelectorException: Locator Strategy 'name' is not supported for this session
...
at org.openqa.selenium.remote.RemoteWebDriver.findElementByName(RemoteWebDriver.java:451)
at io.appium.java_client.DefaultGenericMobileDriver.findElementByName(DefaultGenericMobileDriver.java:104)
...
当面临大量使用findElementByName的历史测试脚本时,手动逐一修改将是一项耗时且容易出错的工作。因此,有必要寻找一种高效的自动化迁移方案。
迁移方案:转向`findElementByAndroidUIAutomator`
针对Android平台,Appium推荐使用findElementByAndroidUIAutomator作为主要的定位策略之一。它可以利用Android原生的UI Automator框架提供的选择器,实现强大而灵活的元素查找。对于原来通过"名称"定位的场景,通常可以映射到UI Automator的text()属性选择器。即,将name属性的值视为元素的可见文本内容。
定位策略转换示例
原始代码(Appium旧版本):
driver.findElementByName("取件").click();
driver.findElementByName(itemDescription).getText();
迁移后的代码(Appium新版本,使用`UIAutomator`):
将硬编码字符串替换为text()选择器:
driver.findElementByAndroidUIAutomator("new UiSelector().text(\"取件\")").click();
将变量替换为text()选择器,并拼接字符串:
driver.findElementByAndroidUIAutomator("new UiSelector().text(\"" + itemDescription + "\")").getText();
注意,new UiSelector().text("...")是使用UI Automator API的完整形式,它提供了更明确和健壮的定位能力。
自动化迁移工具实现
为了批量处理现有代码库中的定位器,我们可以开发一个Java工具类来扫描并修改所有相关的Java文件。这个工具需要能够递归遍历项目目录,读取每个Java文件的内容,然后使用正则表达式进行精确的字符串替换。
Java代码示例:Appium定位器迁移工具
以下是一个用于批量修改Appium定位器语法的Java工具类。它能处理两种情况:定位值是硬编码字符串和定位值是Java变量。
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AppiumLocatorMigrator {
private static int totalMigrations = 0;
public static void main(String[] args) {
// 配置需要扫描的项目根目录
File projectBaseDirectory = new File("D:\\AutoTest\\autotest\\src");
// 建议在运行前备份您的项目代码!
List<String> javaSourceFiles = collectJavaSourceFiles(projectBaseDirectory);
for (String filePath : javaSourceFiles) {
System.out.println("处理文件: " + filePath);
String fileOriginalContent = readSourceFile(filePath);
if (fileOriginalContent != null) {
String fileModifiedContent = performLocatorMigration(fileOriginalContent);
if (!fileModifiedContent.equals(fileOriginalContent)) {
writeModifiedContentToFile(filePath, fileModifiedContent);
System.out.println(" - 文件修改完成: " + filePath);
} else {
System.out.println(" - 文件无需修改: " + filePath);
}
}
}
System.out.println("所有Java文件处理完毕。总计执行了 " + totalMigrations + " 次定位器迁移。");
}
/**
* 递归收集指定目录下所有 .java 文件的绝对路径。
*
* @param directory 要开始遍历的目录。
* @return 包含所有Java文件路径的列表。
*/
public static List<String> collectJavaSourceFiles(File directory) {
List<String> paths = new ArrayList<>();
File[] children = directory.listFiles();
if (children != null) {
for (File item : children) {
if (item.isFile() && item.getName().endsWith(".java")) {
paths.add(item.getAbsolutePath());
} else if (item.isDirectory()) {
paths.addAll(collectJavaSourceFiles(item)); // 递归调用处理子目录
}
}
}
return paths;
}
/**
* 读取文件的全部内容。
*
* @param filePath 文件的路径。
* @return 文件内容的字符串表示,如果读取失败则返回 null。
*/
private static String readSourceFile(String filePath) {
StringBuilder contentBuffer = new StringBuilder();
try (BufferedReader fileReader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = fileReader.readLine()) != null) {
contentBuffer.append(line).append(System.lineSeparator());
}
} catch (IOException e) {
System.err.println("读取文件时发生错误 " + filePath + ": " + e.getMessage());
return null;
}
return contentBuffer.toString();
}
/**
* 执行定位器迁移的核心逻辑,将 'findElementByName' 替换为 'findElementByAndroidUIAutomator'。
*
* @param inputContent 原始文件内容。
* @return 修改后的文件内容。
*/
private static String performLocatorMigration(String inputContent) {
String outputContent = inputContent;
// 1. 替换硬编码的字符串定位器:driver.findElementByName("Some Text")
// 匹配模式:driver.findElementByName("任意非引号字符")
Pattern literalPattern = Pattern.compile("driver\\.findElementByName\\(\"([^\"]*)\"\\)");
Matcher literalMatcher = literalPattern.matcher(outputContent);
StringBuffer literalBuffer = new StringBuffer();
while (literalMatcher.find()) {
totalMigrations++;
// 替换为 driver.findElementByAndroidUIAutomator("new UiSelector().text(\"Some Text\")")
literalMatcher.appendReplacement(literalBuffer,
"driver.findElementByAndroidUIAutomator(\"new UiSelector().text(\\\\\"" + literalMatcher.group(1) + "\\\\\\")\")");
}
literalMatcher.appendTail(literalBuffer);
outputContent = literalBuffer.toString();
// 2. 替换变量作为定位器的场景:driver.findElementByName(someVariable)
// 匹配模式:driver.findElementByName(任意非括号内容,非贪婪匹配)
Pattern variablePattern = Pattern.compile("driver\\.findElementByName\\(([^)]+?)\\)");
Matcher variableMatcher = variablePattern.matcher(outputContent);
StringBuffer variableBuffer = new StringBuffer();
while (variableMatcher.find()) {
totalMigrations++;
// 替换为 driver.findElementByAndroidUIAutomator("new UiSelector().text(\"" + someVariable + "\")")
variableMatcher.appendReplacement(variableBuffer,
"driver.findElementByAndroidUIAutomator(\"new UiSelector().text(\\\"\" + " + variableMatcher.group(1) + " + \"\\\")\")");
}
variableMatcher.appendTail(variableBuffer);
outputContent = variableBuffer.toString();
return outputContent;
}
/**
* 将修改后的内容写回文件。
*
* @param filePath 文件的路径。
* @param content 要写入文件的内容。
*/
private static void writeModifiedContentToFile(String filePath, String content) {
try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filePath))) {
fileWriter.write(content);
} catch (IOException e) {
System.err.println("写入文件时发生错误 " + filePath + ": " + e.getMessage());
}
}
}