Java+Appium移动端自动化测试:从环境搭建到框架设计的工程化实践
1. 项目概述为什么选择JavaAppium如果你正在做移动端测试或者想从功能测试转向自动化那么“Java Appium”这个组合你肯定绕不开。我做了快十年的移动端自动化从早期的MonkeyTalk、Robotium到后来的Appium一路踩坑过来。现在市面上Python做Appium的教程很多但很多中大型互联网公司的测试框架尤其是那些需要和CI/CD深度集成、对稳定性和工程化要求极高的项目后端核心依然是Java。这不是说Python不好Python在快速原型和脚本编写上优势明显但Java在类型安全、多线程管理、成熟的企业级测试框架集成如TestNG、JUnit以及庞大的社区生态方面确实能提供更“稳”的基石。简单说用Java搞Appium自动化测试核心价值在于工程化和可持续性。你写的不是一个跑完就扔的脚本而是一个易于维护、易于扩展、易于与DevOps流程集成的测试资产。它解决的不仅仅是“能不能自动点一点”的问题更是“如何高效、可靠、低成本地保障大量移动应用回归测试”的问题。无论你是测试开发新手还是有一定经验想构建更健壮框架的同学这篇文章都会带你从环境搭建到框架设计完整走一遍。2. 环境搭建与避坑指南环境搭建是劝退新手的第一个门槛Appium涉及Node.js、JDK、Android SDK/或Xcode、Appium Server以及各种客户端依赖。网上教程很多但照着做却跑不通是常态。这里我结合最近帮团队新人配置环境的经验梳理一份针对Java环境的“避坑版”配置清单。2.1 核心组件安装与版本对齐版本冲突是环境问题的万恶之源。务必确保以下核心组件的版本相互兼容。1. Java Development Kit (JDK):推荐使用JDK 8或JDK 11LTS长期支持版。虽然JDK 17更新但一些旧的库或构建工具可能存在兼容性问题。我目前团队稳定使用JDK 11。安装后验证打开命令行输入java -version和javac -version确保版本一致且指向同一个JDK安装。环境变量JAVA_HOME必须正确设置指向JDK安装目录不是JRE目录。Path中需包含%JAVA_HOME%\bin。常见坑如果你遇到类似java: 警告: 源发行版 17 需要目标发行版 17的错误这通常是因为IDE如IntelliJ IDEA或构建工具如Maven中设置的Java编译器版本与项目SDK版本或JAVA_HOME不一致。需要在IDE的Project Structure和Settings中将Project SDK、Project language level以及Modules的Language level统一设置为你的JDK版本如11。2. Appium Server:Appium 2.x 版本架构有了很大变化插件需要独立安装这带来了灵活性也增加了初学者的复杂度。安装通过Node.js的npm安装npm install -g appium。安装后使用appium -v检查版本。驱动管理Appium 2.x 将不同平台的驱动如UiAutomator2 for Android, XCUITest for iOS作为插件管理。这是与1.x版本最大的不同。你必须手动安装所需驱动# 安装Android UIAutomator2驱动 appium driver install uiautomator2 # 安装iOS XCUITest驱动 appium driver install xcuitest插件管理如果你看到[appium] no plugins have been installed. use the appium plugin command to这类提示是正常的说明没有安装额外插件如图像识别插件。如果需要再用appium plugin install命令安装。对于基础自动化先不装插件也行。版本选择新手如果被2.x的插件搞晕可以暂时使用Appium 1.x的最后一个稳定版如1.22.3命令是npm install -g appium1.22.3。但长远看建议拥抱2.x。3. Android开发环境:Android SDK:推荐通过Android Studio安装它会管理SDK和工具链。确保安装你测试应用所需的API Level的Platform-Tools和Build-Tools。环境变量设置ANDROID_HOME指向SDK根目录并在Path中添加%ANDROID_HOME%\platform-tools和%ANDROID_HOME%\tools。关键工具adb(Android Debug Bridge) 是核心。在命令行输入adb devices确保能识别出已连接的手机或模拟器。如果设备列表为空检查USB调试是否开启、驱动是否安装。4. 构建工具与IDE:Maven/Gradle:项目管理必备。我习惯用Mavenpom.xml文件能清晰管理依赖。IDE:IntelliJ IDEA 是Java开发的首选对Maven和测试框架的支持非常好。2.2 依赖配置与项目初始化环境工具就绪后我们创建一个Maven项目来管理代码和依赖。创建Maven项目在IDEA中新建项目选择Maven。配置pom.xml这是项目的核心配置文件需要添加Appium Java客户端依赖和测试框架依赖。dependencies !-- Appium Java Client -- dependency groupIdio.appium/groupId artifactIdjava-client/artifactId version8.5.0/version !-- 使用较新版本兼容Appium 2.x -- /dependency !-- 测试框架 - 推荐TestNG功能更强大 -- dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.8.0/version scopetest/scope /dependency !-- 日志框架便于排查问题 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.7/version scopetest/scope /dependency /dependencies解决Lombok警告如遇如果你在项目中使用Lombok来简化POJO类如封装Page ObjectIDEA可能会提示java: you aren‘t using a compiler supported by lombok, so lombok will not work。这不是运行时错误但会导致Getter/Setter不生效。解决方法在IDEA中安装Lombok插件并在设置中启用注解处理Settings - Build, Execution, Deployment - Compiler - Annotation Processors勾选Enable annotation processing。实操心得环境配置一次成功是小概率事件。建议建立一个团队内部的“环境配置清单”文档记录所有软件的具体版本号和下载链接。新人入职时直接按文档步骤操作能节省大量排查时间。另外对于Android真机不同品牌手机开启“开发者选项”和“USB调试”的方式差异很大最好也整理进去。3. 核心原理与关键对象解析在开始写第一行测试代码前理解Appium的工作原理和几个核心对象至关重要。这能让你在遇到元素找不到、脚本运行失败时知道该从哪里入手排查。3.1 Appium的工作机制WebDriver协议与中间层Appium的核心哲学是“不重新发明轮子”。它没有自己创造一套新的自动化协议而是基于标准的WebDriver协议又称JSON Wire Protocol。这个协议本来是用于Web浏览器自动化的SeleniumAppium将其扩展到了移动端。它的工作流程可以简单理解为你的测试脚本Java Client发起一个请求例如“查找ID为‘login’的元素”。Appium Server接收到这个请求它扮演一个中间层或翻译官的角色。Appium Server根据你指定的平台Android/iOS将请求“翻译”成该平台原生测试框架能听懂的命令。对于Android它调用UiAutomator2或Espresso对于iOS它调用XCUITest。原生测试框架在手机或模拟器上执行实际的操作点击、滑动、获取文本。结果再原路返回给你的测试脚本。这种架构的好处是你只需要学习一套WebDriver API在Java里就是io.appium.java_client包下的类就可以同时控制Android和iOS设备。这也是为什么你会看到AndroidDriver和IOSDriver都继承自同一个父类AppiumDriver。3.2 关键对象DesiredCapabilities与AppiumDriver这是你编写脚本时打交道最多的两个类。1.DesiredCapabilities 会话配置清单你可以把它想象成一份“需求说明书”告诉Appium Server你想要启动一个什么样的自动化会话。它是一组键值对的集合。配置错误是脚本无法启动的最常见原因。DesiredCapabilities caps new DesiredCapabilities(); caps.setCapability(“platformName”, “Android”); // 平台Android 或 iOS caps.setCapability(“platformVersion”, “12”); // 手机系统版本 caps.setCapability(“deviceName”, “Pixel_5_API_31”); // 设备名称adb devices 看到的 caps.setCapability(“automationName”, “UiAutomator2”); // 自动化引擎Android必选 caps.setCapability(“appPackage”, “com.example.myapp”); // 被测App的包名 caps.setCapability(“appActivity”, “.MainActivity”); // 被测App的启动Activity // caps.setCapability(“app”, “/path/to/your/app.apk”); // 如果安装新App用这个关键点automationName在Appium 2.x中尤为重要必须明确指定为“UiAutomator2”Android或“XCUITest”iOS。deviceName对于Android来说不一定需要完全精确但最好与adb devices列表中的一致。2.AppiumDriver及其子类 控制设备的遥控器这个对象是你控制手机/模拟器的入口。所有后续的查找元素、操作元素都通过它进行。// 通常使用其子类类型更明确 AndroidDriver driver new AndroidDriver(new URL(“http://127.0.0.1:4723/wd/hub”), caps); // 对于iOS // IOSDriver driver new IOSDriver(new URL(“http://127.0.0.1:4723/wd/hub”), caps);URL指向你启动的Appium Server地址默认是本地4723端口。初始化时机通常在每个测试类开始前BeforeSuite或BeforeClass初始化Driver在所有测试结束后AfterSuite或AfterClass调用driver.quit()来关闭会话释放资源。务必记得quit否则Appium Server会残留会话导致后续测试失败。3.3 元素定位策略八种武器找到界面元素是自动化的第一步。Appium支持多种定位方式和Selenium类似但也有一些移动端特有的。定位方式示例 (Java)适用场景与优缺点ID (resource-id)driver.findElement(By.id(“com.app:id/username”))首选。Android对应resource-idiOS对应name或accessibility id。通常唯一定位最快最稳定。Accessibility IDdriver.findElement(By.accessibilityId(“LoginButton”))次选。对应Android的content-desc和iOS的accessibility identifier。专为无障碍测试设计语义化好。XPathdriver.findElement(By.xpath(“//android.widget.Button[text‘登录’]”))慎用。功能最强大可以组合各种属性。但性能最差且对UI结构变化极其敏感维护成本高。仅在无ID和Accessibility ID时使用相对路径。Class Namedriver.findElement(By.className(“android.widget.EditText”))通常用于查找同一类型的多个元素如所有输入框。很少能唯一确定一个元素。Android UIAutomatordriver.findElement(By.androidUIAutomator(“new UiSelector().text(‘确定’)”))Android专属非常强大。可以使用UIAutomator API的所有选择器如text,className,resourceId等组合查询。iOS Class Chaindriver.findElement(By.iOSClassChain(“**/XCUIElementTypeButton[name ‘Submit‘]”))iOS专属类似XPath但性能更好。iOS Predicate Stringdriver.findElement(By.iOSNsPredicateString(“type ‘XCUIElementTypeButton’ AND name ‘下一步’“))iOS专属功能强大语法灵活性能优于XPath。CSS Selector (仅WebView)driver.findElement(By.cssSelector(“#web_login_btn”))仅适用于App内的WebView/H5页面。需要先切换上下文到WebView。注意事项定位元素时绝对不要依赖坐标。不同设备分辨率不同坐标会变。优先使用ID和Accessibility ID。使用XPath时尽量避免使用绝对路径以/开头和索引如[1]多使用元素属性和相对路径以提高脚本的健壮性。4. 测试框架设计与最佳实践直接写一堆散乱的测试方法很快就会变得难以维护。我们需要一个好的框架设计。这里我推荐“Page Object Model (POM) TestNG 数据驱动”的组合这是目前Java生态中最成熟、最主流的模式。4.1 Page Object Model让代码更清晰POM的核心思想是将测试脚本和页面对象分离。每个App的页面或一个主要功能模块对应一个Java类这个类中封装了该页面的所有元素定位符和对这些元素的操作方法如输入、点击。测试脚本里只包含业务逻辑和断言不再直接出现findElement这类底层代码。一个简单的登录页面对象示例// LoginPage.java public class LoginPage { private AndroidDriver driver; // 1. 定义页面元素定位符 FindBy(id “com.app:id/et_username”) private MobileElement usernameField; FindBy(id “com.app:id/et_password”) private MobileElement passwordField; FindBy(id “com.app:id/btn_login”) private MobileElement loginButton; FindBy(id “com.app:id/tv_error_toast”) private MobileElement errorToast; // 2. 构造函数初始化元素 public LoginPage(AndroidDriver driver) { this.driver driver; // 使用AppiumFieldDecorator来支持FindBy等注解的延迟查找 PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(10)), this); } // 3. 封装页面操作方法 public void enterUsername(String username) { usernameField.clear(); usernameField.sendKeys(username); } public void enterPassword(String password) { passwordField.clear(); passwordField.sendKeys(password); } public void clickLogin() { loginButton.click(); } public String getErrorToastText() { // 等待Toast出现并获取文本 return errorToast.getText(); } // 4. 组合业务方法 public HomePage loginWithValidCreds(String user, String pwd) { enterUsername(user); enterPassword(pwd); clickLogin(); // 返回下一个页面对象实现链式调用 return new HomePage(driver); } public void loginWithInvalidCreds(String user, String pwd) { enterUsername(user); enterPassword(pwd); clickLogin(); // 停留在本页面用于验证错误提示 } }POM的好处高可维护性UI元素定位符只在一处定义。如果登录按钮的ID变了你只需要修改LoginPage.java文件中的一处。高可读性测试脚本读起来像自然语言loginPage.loginWithValidCreds(“admin”, “123456”)。低冗余页面操作方法被复用避免了测试脚本中的代码重复。4.2 使用TestNG组织测试用例TestNG比JUnit功能更强大更适合做自动化测试。它提供了更灵活的测试套件配置、依赖管理、分组、参数化等特性。基础测试类结构// LoginTest.java public class LoginTest { private AndroidDriver driver; private LoginPage loginPage; BeforeClass public void setUp() throws MalformedURLException { // 初始化DesiredCapabilities DesiredCapabilities caps new DesiredCapabilities(); caps.setCapability(“platformName”, “Android”); caps.setCapability(“deviceName”, “emulator-5554”); caps.setCapability(“automationName”, “UiAutomator2”); caps.setCapability(“appPackage”, “com.example.myapp”); caps.setCapability(“appActivity”, “.LoginActivity”); // 初始化Driver driver new AndroidDriver(new URL(“http://127.0.0.1:4723”), caps); // 初始化页面对象 loginPage new LoginPage(driver); } Test(priority 1, description “测试有效登录”) public void testValidLogin() { HomePage homePage loginPage.loginWithValidCreds(“validUser”, “validPass”); // 断言验证是否成功跳转到首页 Assert.assertTrue(homePage.isWelcomeMessageDisplayed(), “登录成功后未显示欢迎信息”); } Test(priority 2, description “测试空密码登录”) public void testLoginWithEmptyPassword() { loginPage.loginWithInvalidCreds(“validUser”, “”); String errorMsg loginPage.getErrorToastText(); Assert.assertEquals(errorMsg, “密码不能为空”, “错误提示信息不符”); } AfterClass public void tearDown() { if (driver ! null) { driver.quit(); } } }4.3 数据驱动测试当需要用多组数据测试同一个功能时如测试登录需要测正确密码、错误密码、空用户名等硬编码在测试方法里很糟糕。TestNG的DataProvider是解决这个问题的利器。public class LoginDataDrivenTest { private LoginPage loginPage; // ... setUp 和 tearDown 省略 ... // 1. 定义数据提供者 DataProvider(name “loginData”) public Object[][] provideLoginData() { return new Object[][] { { “admin”, “admin123”, true, “登录成功” }, // 用户名密码是否成功描述 { “admin”, “wrong”, false, “密码错误” }, { “”, “admin123”, false, “用户名为空” }, { “admin”, “”, false, “密码为空” }, }; } // 2. 测试方法使用数据提供者 Test(dataProvider “loginData”) public void testLoginWithMultipleData(String username, String password, boolean expectedSuccess, String description) { System.out.println(“Testing: ” description); if (expectedSuccess) { HomePage homePage loginPage.loginWithValidCreds(username, password); Assert.assertTrue(homePage.isWelcomeMessageDisplayed()); } else { loginPage.loginWithInvalidCreds(username, password); // 这里可以更精细地断言不同的错误提示 Assert.assertNotNull(loginPage.getErrorToastText()); } } }数据也可以从外部文件如Excel、JSON、CSV读取使测试数据与代码完全分离维护起来更方便。5. 高级技巧与稳定性提升写一个能跑的脚本不难写一个能在不同设备、不同网络环境下稳定运行的脚本才是挑战。下面分享几个提升脚本稳定性和效率的实战技巧。5.1 智能等待告别Thread.sleep使用Thread.sleep(5000)是自动化脚本的“毒药”。它固定等待无论元素是否已出现都会傻等极大拖慢测试速度且不可靠。正确的做法是使用“显式等待”。import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; // 创建一个WebDriverWait对象设置最大等待时间10秒轮询间隔500毫秒 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); // 等待元素可点击 MobileElement submitButton driver.findElement(By.id(“submit”)); wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click(); // 等待元素出现 MobileElement successMsg wait.until(ExpectedConditions.presenceOfElementLocated(By.id(“success”))); Assert.assertEquals(successMsg.getText(), “操作成功”); // 等待元素消失如等待Loading框消失 wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id(“loading”)));隐式等待 vs 显式等待隐式等待Implicit Waitdriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));设置一个全局的等待时间在查找任何元素时如果没立刻找到会轮询查找直到超时。它只对findElement生效。通常不推荐和显式等待混用容易导致不可预期的超时。显式等待Explicit Wait如上例针对某个特定条件如元素可点击、元素可见进行等待。这是推荐的最佳实践更精确更高效。5.2 处理弹窗、权限请求和Toast移动端测试中系统弹窗如网络权限、位置权限和App内的Toast提示是常见的干扰项。1. 处理系统弹窗/权限请求这些弹窗属于系统UI不在你的App上下文内。Appium提供了一种切换到原生上下文NATIVE_APP来处理它们的方法。// 获取当前所有可用的上下文 SetString contextHandles driver.getContextHandles(); for (String context : contextHandles) { System.out.println(context); // 通常能看到 “NATIVE_APP” 和 “WEBVIEW_com.example.myapp” } // 切换到原生上下文以操作系统弹窗 driver.context(“NATIVE_APP”); // 定位并点击系统弹窗的“允许”按钮定位方式需用UIAutomator Viewer或Appium Inspector查看 driver.findElement(By.id(“com.android.packageinstaller:id/permission_allow_button”)).click(); // 操作完成后切回你的App上下文 driver.context(“WEBVIEW_com.example.myapp”); // 如果是H5或切回默认 // 对于纯原生App通常不需要显式切回因为NATIVE_APP就是默认上下文。2. 捕获Toast消息Toast是Android特有的短暂提示。它属于系统层级的View生命周期短。定位Toast的关键是使用android.widget.Toast这个类名并且需要快速捕获。// 在触发Toast的操作后立即尝试定位 // 使用显式等待但超时时间可以设短一点比如3秒 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(3)); try { MobileElement toastElement wait.until(ExpectedConditions.presenceOfElementLocated( By.xpath(“//android.widget.Toast[1]”) // 定位第一个Toast )); String toastText toastElement.getText(); System.out.println(“捕获到Toast: ” toastText); Assert.assertEquals(toastText, “登录成功”); } catch (TimeoutException e) { Assert.fail(“未捕获到预期的Toast消息”); }5.3 截图与日志排查问题的利器测试失败时光看日志不够直观。自动截图和结构化日志是定位问题的关键。1. 测试失败自动截图利用TestNG的ITestListener监听器接口可以在测试失败时自动触发截图。// 1. 创建一个监听器类 public class TestListener implements ITestListener { Override public void onTestFailure(ITestResult result) { // 获取当前测试类的driver对象需要想办法传递比如用ThreadLocal存储 AndroidDriver driver YourBaseTestClass.getDriver(); // 假设BaseTest里有获取driver的方法 if (driver ! null) { try { // 截图并保存为文件 File screenshot driver.getScreenshotAs(OutputType.FILE); String fileName “screenshot_” result.getName() “_” System.currentTimeMillis() “.png”; File destFile new File(“./test-output/screenshots/” fileName); FileUtils.copyFile(screenshot, destFile); System.out.println(“测试失败截图已保存至: ” destFile.getAbsolutePath()); // 也可以将截图路径附加到测试报告中 result.setAttribute(“screenshot”, destFile.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } } } // ... 可以重写其他方法如onTestSuccess, onStart等 } // 2. 在测试类上使用Listeners注解或通过testng.xml文件配置监听器 Listeners(TestListener.class) public class YourTestClass { ... }2. 使用SLF4J记录结构化日志在pom.xml中我们已经引入了slf4j-simple。在代码中合理使用日志可以清晰看到测试执行流程。import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LoginPage { private static final Logger logger LoggerFactory.getLogger(LoginPage.class); public HomePage loginWithValidCreds(String user, String pwd) { logger.info(“开始登录操作用户名: {}”, user); // 使用占位符{}避免字符串拼接 enterUsername(user); // 密码通常不打日志 enterPassword(pwd); logger.debug(“点击登录按钮”); clickLogin(); logger.info(“登录操作完成预期跳转至首页”); return new HomePage(driver); } }配置simplelogger.properties文件可以控制日志级别和输出格式让日志更清晰。6. 持续集成与实战排坑个人跑通只是第一步让自动化测试在团队中持续运行产生价值需要集成到CI/CD流水线中。6.1 集成到Jenkins/GitLab CI核心思路是将你的Maven项目放到代码仓库GitCI工具在每次代码推送后自动拉取代码执行测试命令并收集测试报告。一个简单的Jenkins Freestyle项目配置源码管理配置Git仓库地址和分支。构建触发器可以配置轮询SCM或Webhook。构建步骤增加一个“Execute shell”或“Invoke top-level Maven targets”步骤。# 示例Shell命令 # 1. 确保Appium Server已启动可以在Jenkins服务器上以后台服务方式运行 # 2. 连接设备或启动模拟器 # 3. 执行测试 mvn clean test -DtestLoginTest后置操作配置发布JUnit/TestNG测试报告。TestNG默认会在test-output目录生成index.html和emailable-report.html。使用Jenkins的“Publish JUnit test result report”插件指定XML报告路径如test-output/testng-results.xmlJenkins就能图形化展示测试结果和趋势。在CI中运行的关键点环境一致性CI服务器上的JDK、Appium、Android SDK版本必须与开发环境一致。设备/模拟器管理可以使用云测平台如HeadSpin AWS Device Farm的真机或者在CI服务器上使用Android模拟器需要开启KVM加速并妥善管理模拟器生命周期。稳定性CI环境下的测试需要更高的稳定性。前面提到的智能等待、失败重试机制TestNG的Test(retryAnalyzer ...)、异常处理就尤为重要。6.2 常见问题排查清单实战排坑即使一切配置正确脚本也可能在某个时刻失败。下面是一个快速排查清单现象可能原因排查步骤SessionNotCreatedException1.DesiredCapabilities配置错误。2. Appium Server与客户端版本不兼容。3. 设备未连接或未就绪。1. 逐项检查Capabilities特别是appPackage/appActivity或app路径、automationName。2. 检查Appium Server日志启动时加--log-level debug看具体错误。3. 运行adb devices确认设备在线。NoSuchElementException1. 元素定位符写错。2. 页面未加载完成。3. 元素在WebView或混合应用中上下文未切换。1. 使用Appium Inspector或UIAutomatorViewer重新检查元素属性。2. 添加显式等待等待元素出现。3. 打印当前所有上下文driver.getContextHandles()并切换到正确的上下文。元素找到了但点击没反应1. 元素不可点击被遮挡、未enable。2. 点错了位置如点了元素边缘。3. 需要特殊操作如长按。1. 改用ExpectedConditions.elementToBeClickable等待。2. 尝试使用TouchAction或W3C ActionsAPI进行精确点击。3. 使用driver.executeScript(“mobile: tap”, params)或TouchAction的长按方法。脚本在本地跑得通在CI上失败1. 环境差异版本、路径。2. 设备状态差异屏幕锁屏、弹窗。3. 并发问题多任务抢占设备。1. 在CI脚本中增加环境检查步骤输出关键路径和版本。2. 在测试开始前加入解锁屏幕、关闭无关弹窗的代码。3. 使用设备锁或调度系统确保测试串行执行或使用不同设备。UnexpectedAlertPresentException出现了未预期的弹窗系统权限、App更新提示。1. 在setUp方法中预先处理已知弹窗如授权。2. 使用try-catch包裹可能引发弹窗的操作捕获后处理弹窗。测试报告乱码或找不到构建环境的文件编码或路径问题。1. 在Maven的surefire-plugin配置中指定报告输出编码为UTF-8。2. 在Jenkins中指定报告XML文件的绝对路径。最后保持耐心和细心。移动自动化测试尤其是涉及不同设备和OS版本时就是一个不断与“不确定性”斗争的过程。建立一个稳定的测试环境编写健壮、容错的测试代码并善用日志和截图能帮你把“不确定性”降到最低。从一个小模块开始实践POM逐步搭建你的测试框架你会发现用Java做Appium自动化虽然起步可能比Python稍复杂一点但后期在维护和扩展上的优势会让你觉得这一切都是值得的。