当前位置:首页 > 技术 > 正文内容

Appium Android自动化:从`findElementByName`到`UIAutomator`定位策略的迁移实践

访客 技术 2026年7月9日 1

在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());
        }
    }
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。