尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Appium UiAutomator2驱动详解:无线自动化测试与并行执行实战

Appium UiAutomator2驱动详解:无线自动化测试与并行执行实战 1. 项目概述与核心价值如果你正在用Appium做Android自动化测试还在为每次测试都必须插着USB线、处理ADB连接不稳定、多设备并行时端口冲突而头疼那么今天聊的uiautomator2驱动就是你跳出这个“坑”的关键一步。我做了快十年的移动端自动化从早期的Instrumentation到后来的UIAutomator再到Appium整合可以说uiautomator2是Appium在Android平台上最稳定、功能最强大的驱动没有之一。它不仅仅是把Google的UIAutomator2框架包装了一下而是深度融合了Appium的跨平台理念提供了远超原生框架的稳定性和扩展能力。这个项目的核心目标很明确让你彻底摆脱对USB线缆和ADB有线连接的强依赖实现更稳定、可并行、易维护的Android自动化测试。听起来是不是有点“无线自由”的感觉但这背后远不止是拔掉一根线那么简单。它意味着你可以把测试机丢进机房柜子里通过Wi-Fi网络远程执行用例意味着你可以同时控制十几台设备做兼容性测试而不用担心USB Hub的供电和识别问题也意味着你的自动化脚本能更容易地集成到CI/CD流水线中在云端动态分配执行。而实现这一切的基石就是Appium UiAutomator2 Driver对Android底层自动化能力的深度封装和网络化扩展。接下来我会结合我趟过的无数个坑带你从原理到实操彻底吃透它。2. UiAutomator2驱动深度解析为什么是它在Appium的生态里Android端的驱动有过好几个比如最早的UiAutomator1已废弃、Espresso以及我们今天的主角UiAutomator2。为什么我强烈推荐你使用UiAutomator2这得从它的架构设计说起。2.1 核心架构从ADB到HTTP的桥梁传统的UIAutomator测试脚本是打包成APK通过adb shell am instrument命令在设备上直接运行的。这种方式紧密耦合难以远程控制。UiAutomator2驱动在架构上做了一个巧妙的拆分UiAutomator2 Server (APK)这是一个安装在测试设备上的Android应用通常叫io.appium.uiautomator2.server。它的核心是一个HTTP服务运行在设备内部。这个Server接管了与Android系统UiAutomator框架的所有交互包括查找元素、执行操作、获取页面结构等。Appium Server运行在你的电脑或服务器上。它通过ADB的端口转发Port Forwarding能力将本地的一个端口如8200映射到设备上UiAutomator2 Server的端口默认6790。你的测试脚本 (Client)脚本用Python、Java等编写使用WebDriver协议向本机的Appium Server例如http://localhost:4723发送HTTP请求。Appium Server再将请求通过ADB转发给设备内的UiAutomator2 Server执行。这个架构的精妙之处在于将控制逻辑你的脚本和执行引擎UiAutomator2 Server进行了物理和逻辑上的解耦。只要设备与Appium Server之间存在某种连接USB ADB 或 无线ADB控制指令就能送达。这就为“摆脱USB”提供了理论基础我们只需要确保ADB连接是通的至于这个连接是USB线还是Wi-Fi对于上层的Appium和你的脚本来说是透明的。注意这里说的“摆脱USB”是指摆脱对USB线缆物理连接的持续依赖而不是完全不用ADB。初始的设备发现、无线连接建立、UiAutomator2 Server APK的安装通常还是需要一次USB连接或网络配置。一旦建立好无线ADB连接后续的测试执行就可以完全脱离USB线。2.2 相较于UiAutomator1的核心优势很多从老版本迁移过来的同学会问升级的必要性是什么除了官方已停止维护UiAutomator1外UiAutomator2在技术和体验上带来了质的飞跃稳定性与性能UiAutomator1基于Android的Instrumentation在页面频繁变化时容易丢失元素句柄。UiAutomator2直接基于Android系统自带的UiAutomation框架更底层对UI变化的容忍度更高执行速度也更快。丰富的元素属性UiAutomator2能获取到的元素属性远超UiAutomator1。除了基本的text、resource-id、class还能拿到bounds坐标、content-desc、checkable、long-clickable等甚至包括一些辅助功能相关的属性为编写更健壮的选择器提供了极大便利。更好的WebView支持对于混合应用Hybrid App中的WebView内容UiAutomator2通过集成ChromeDriver提供了更稳定、标准化的支持可以像在浏览器中一样操作Web元素。强大的扩展命令这是UiAutomator2的杀手锏。它提供了一系列mobile:前缀的扩展命令如mobile: shell,mobile: scroll,mobile: deepLink让你能直接执行ADB命令、实现复杂滚动、触发深层链接等极大地扩展了自动化的边界。这些在UiAutomator1里要么很难实现要么完全不行。2.3 关键能力并行测试与网络化执行这是实现“摆脱USB”场景的关键。UiAutomator2驱动在设计之初就考虑到了并行。独立的系统端口每个设备上的UiAutomator2 Server需要监听一个端口。为了避免冲突在创建Session时必须通过systemPort能力Capability为每个会话指定一个唯一的端口号。例如设备A用8200设备B用8201。Appium Server会通过ADB分别将这些端口映射到设备本地。会话隔离每个Appium会话对应一个独立的UiAutomator2 Server进程严格来说是同一个APK的不同实例。这意味着同时运行的多组测试不会相互干扰。无线ADB连接一旦你通过adb tcpip 5555和adb connect 设备IP:5555命令建立了与设备的无线ADB连接后续的Appium通信包括端口转发、安装APK、启动Session就都可以通过Wi-Fi网络进行。你的测试机可以放在任何有网络的地方。一个典型的并行测试配置示例 假设你有两台设备IP分别是192.168.1.100和192.168.1.101你已经分别建立了无线ADB连接。# 设备1的配置 capabilities_device1 { platformName: Android, appium:platformVersion: 13, appium:deviceName: Pixel_6, appium:automationName: UiAutomator2, # 指定使用UiAutomator2驱动 appium:app: /path/to/your/app.apk, appium:udid: 192.168.1.100:5555, # 使用无线ADB的设备标识 appium:systemPort: 8200, # 必须唯一 appium:chromedriverPort: 9515, # 如果测试WebView此端口也需唯一 } # 设备2的配置 capabilities_device2 { platformName: Android, appium:platformVersion: 13, appium:deviceName: Galaxy_S22, appium:automationName: UiAutomator2, appium:app: /path/to/your/app.apk, appium:udid: 192.168.1.101:5555, appium:systemPort: 8201, # 与设备1不同 appium:chromedriverPort: 9516, }然后你可以用Python的threading或测试框架如pytest的并行插件同时向同一个Appium Server或不同端口的多个Appium Server实例发起连接实现真正的并行执行。3. 环境搭建与无线连接实战理论懂了我们上手把环境跑通。目标是在一台已通过USB初始化的设备上建立稳定的无线ADB连接并配置好Appium UiAutomator2测试环境。3.1 基础环境准备安装Appium Server推荐使用Appium 2.x。它采用插件化架构更轻量。npm install -g appiumnext appium driver install uiautomator2 # 安装UiAutomator2驱动插件 appium server install --sourcenpm appium # 安装Appium Server安装Python客户端pip install Appium-Python-Client确保ADB可用Android SDK Platform-Tools中的adb命令需要加入系统PATH。3.2 建立无线ADB连接关键步骤这是“摆脱USB”的核心操作。请确保你的测试设备和电脑在同一个局域网内。步骤一USB初始化第一次用USB线连接设备和电脑确保adb devices能识别到设备状态为device。步骤二开启设备的无线调试端口adb tcpip 5555执行成功会显示restarting in TCP mode port: 5555。这个命令会重启设备上的ADB守护进程并监听5555端口。步骤三断开USB连接无线拔掉USB线。获取设备的IP地址通常在设置-关于手机-状态信息里。然后使用adb connect 设备IP:5555 # 例如adb connect 192.168.1.100:5555连接成功会显示connected to 192.168.1.100:5555。再次运行adb devices你应该能看到一个以IP地址开头的设备条目。步骤四验证无线控制尝试一个简单的ADB命令如adb -s 192.168.1.100:5555 shell getprop ro.product.model看是否能正常返回设备型号。实操心得防火墙如果连接失败检查电脑和设备的防火墙是否放行了5555端口。IP变更设备重启或Wi-Fi重连可能导致IP变化。一种更稳定的做法是在路由器中为测试设备分配静态IPDHCP保留。连接保持无线ADB连接有时会超时断开。可以写一个简单的守护脚本定期执行adb connect。或者更高级的做法是使用adb usb切回USB模式再adb tcpip重新激活但这需要脚本能控制USB端口通断需要硬件支持。多设备管理无线连接后adb devices会列出所有设备USB和无线。在Appium的Capabilities中务必使用udid字段精确指定你要控制的设备如192.168.1.100:5555。3.3 编写你的第一个“无线”测试脚本现在我们用Python写一个简单的脚本通过无线连接启动一个App。from appium import webdriver from appium.options.android import UiAutomator2Options import time # 1. 定义Capabilities使用UiAutomator2Options更现代 options UiAutomator2Options() options.platform_name Android options.device_name AnyName # 在无线连接下这个名字不重要udid才是关键 options.automation_name UiAutomator2 options.app /Users/yourname/Downloads/my_app.apk # 或者使用 appPackage 和 appActivity # options.app_package com.example.myapp # options.app_activity .MainActivity # 关键指定无线连接的设备UDID和唯一的系统端口 options.udid 192.168.1.100:5555 options.system_port 8200 # 必须唯一避免端口冲突 # 2. 连接Appium Server假设运行在本机4723端口 driver webdriver.Remote(http://localhost:4723, optionsoptions) try: # 3. 简单的操作示例等待并打印当前页面源码 time.sleep(3) # 等待App启动 page_source driver.page_source print(page_source[:500]) # 打印前500个字符看看 # 4. 找一个元素并点击示例 # 这里需要根据你的App实际UI来写定位器 # element driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, new UiSelector().text(Login)) # element.click() finally: # 5. 退出会话 driver.quit() print(测试结束会话已关闭。)运行这个脚本前请确保Appium Server已启动在终端运行appium或appium server。设备已通过adb connect连接成功。system_port指定的端口如8200没有被其他进程占用。如果一切顺利你会看到设备上的App被启动并且脚本打印出页面的XML结构。恭喜你已经实现了无线自动化4. UiAutomator2核心功能与Python进阶技巧基础打通后我们来深入看看UiAutomator2那些能极大提升你脚本效率和稳定性的高级功能。我会结合Python代码示例来说明。4.1 强大的元素定位策略UiAutomator2支持多种定位策略速度和稳定性各有不同。resource-id (推荐)最稳定、最快的定位方式。对应Android开发中的android:id。from appium.webdriver.common.appiumby import AppiumBy login_button driver.find_element(AppiumBy.ID, com.example.app:id/btn_login)注意AppiumBy.ID在UiAutomator2驱动下内部就是使用resource-id定位器。accessibility id对应元素的contentDescription属性。对于没有resource-id但设置了无障碍标签的元素这是最佳选择。search_box driver.find_element(AppiumBy.ACCESSIBILITY_ID, 搜索框)-android uiautomator (UiSelector)非常灵活可以组合多个条件也支持滚动查找。但请注意官方警告Google未来可能废弃UiSelector等类长期项目慎用。# 通过文本定位 element driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, new UiSelector().text(确定)) # 组合条件类名为Button且可点击 element driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, new UiSelector().className(android.widget.Button).clickable(true)) # 滚动查找经典用法 scrollable_selector new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text(遥远的元素)) element driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, scrollable_selector)XPath功能强大但相对较慢。适用于复杂层级结构定位。从UIA2 Server 4.25.0开始默认支持XPath 2.0功能更强但遇到复杂定位器有问题时可以尝试在Settings中启用enforceXPath1: true回退到XPath 1.0。# 定位包含特定文本的TextView element driver.find_element(AppiumBy.XPATH, //android.widget.TextView[text用户名]) # 定位兄弟节点 element driver.find_element(AppiumBy.XPATH, //android.widget.Button[resource-idbtn1]/following-sibling::android.widget.TextView[1])定位策略选择建议优先使用resource-id其次accessibility id。对于需要滚动查找的动态列表-android uiautomator的UiScrollable目前仍是较优解。复杂静态层级且上述方法无效时再考虑XPath。4.2 必须掌握的Settings APISettings API允许你在会话运行时动态调整UiAutomator2驱动的一些行为这是解决很多疑难杂症的利器。通过driver.update_settings(settings)来设置。waitForIdleTimeout这是最重要的设置之一。它定义了驱动在查找或操作元素前等待UI变为“空闲”状态的最大时间。如果你的App动画较多或响应慢增加这个值可以避免因“元素未找到”而导致的失败。driver.update_settings({waitForIdleTimeout: 15000}) # 设置为15秒allowInvisibleElements是否在页面源码中包含不可见元素。默认false。如果你需要定位一些隐藏的元素如下拉菜单的选项可以设为true但会降低页面获取速度。ignoreUnimportantViews是否压缩布局层级。默认false。设为true可以简化页面源码加快XPath解析但可能会丢失一些中间层容器信息。shouldUseCompactResponses与elementResponseAttributes这两个配合使用。默认shouldUseCompactResponses为truefind_element只返回元素UUID。如果你希望一次查找就获取元素的多个属性如text, enabled可以driver.update_settings({ shouldUseCompactResponses: False, elementResponseAttributes: text,enabled,displayed }) # 这样find_element返回的对象就会直接包含这些属性减少后续获取属性的网络请求。4.3 进阶使用mobile:扩展命令这是UiAutomator2的宝藏功能。通过driver.execute_script(mobile: commandName, arguments)来调用。1. 执行Shell命令 (mobile: shell)让你能在测试中直接执行ADB Shell命令非常强大。注意此功能需要Appium Server启动时启用--allow-insecureadb_shell或配置allow-insecure。# 获取设备当前Activity result driver.execute_script(mobile: shell, { command: dumpsys, args: [window, windows, |, grep, -E, mCurrentFocus] }) print(result) # 输出类似mCurrentFocusWindow{... com.example.app/.MainActivity} # 清除应用数据 driver.execute_script(mobile: shell, { command: pm, args: [clear, com.example.app] })2. 复杂滚动 (mobile: scroll)比UiScrollable更简洁的滚动方式。# 在第一个可滚动容器中滚动直到找到包含“提交”文本的元素 driver.execute_script(mobile: scroll, { strategy: accessibility id, selector: 提交, maxSwipes: 5 })3. 启动深层链接 (mobile: deepLink)直接测试App的Deep Link功能。driver.execute_script(mobile: deepLink, { url: myapp://product/12345, package: com.example.myapp })4. 操作通知栏 (mobile: openNotifications,mobile: getNotifications)# 打开通知栏 driver.execute_script(mobile: openNotifications) time.sleep(1) # 获取所有通知 notifications driver.execute_script(mobile: getNotifications) for notice in notifications.get(statusBarNotifications, []): if 订单发货 in notice.get(notification, {}).get(bigText, ): print(找到物流通知)5. 应用管理 (mobile: installApp,mobile: removeApp,mobile: terminateApp等)# 静默安装APK覆盖安装 driver.execute_script(mobile: installApp, { appPath: /path/to/new_version.apk, grantPermissions: True, # 自动授予权限 replace: True }) # 终止应用 driver.execute_script(mobile: terminateApp, {appId: com.example.app})5. 实战避坑指南与性能优化踩过无数坑后我总结了一些关键问题和解决方案能帮你节省大量调试时间。5.1 常见问题与排查问题1UiAutomator2Server安装失败或启动超时。现象Session创建失败日志提示Unable to launch WebDriverAgent或UIAutomator2 server not started。排查检查设备是否安装了io.appium.uiautomator2.server和io.appium.uiautomator2.server.test这两个APK。可以用adb shell pm list packages | grep appium查看。如果已安装尝试卸载重装adb uninstall io.appium.uiautomator2.server和adb uninstall io.appium.uiautomator2.server.test。然后重启Appium它会自动重装。检查systemPort是否被占用或冲突。确保并行测试时每个Session的systemPort和chromedriverPort都是唯一的。对于无线连接确保无线ADB连接稳定。尝试adb kill-server然后重新adb connect。问题2元素找不到但明明在页面上。现象NoSuchElementException但通过driver.page_source能看到该元素。排查与解决等待问题UI还没加载完。使用显式等待WebDriverWait。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element WebDriverWait(driver, 10).until( EC.presence_of_element_located((AppiumBy.ID, com.example:id/btn)) )上下文问题可能在WebView里却用Native的定位方式或者反之。用driver.contexts和driver.switch_to.context切换上下文。页面结构变化Appium获取的页面结构可能滞后。尝试在查找前刷新页面结构driver.update_settings({waitForIdleTimeout: 500})或使用driver.implicitly_wait(0)临时禁用隐式等待再立即查找。XPath性能过于复杂的XPath在深层级页面中极慢。尽量用ID或accessibility id或使用ignoreUnimportantViews: true简化DOM。问题3手势操作如滑动不生效或行为怪异。现象swipe、scroll没效果或者滑到了奇怪的地方。解决优先使用UiAutomator2提供的mobile:手势命令如mobile: scrollGesture、mobile: swipeGesture它们更底层、更稳定。使用W3C Actions API进行精细控制这是未来趋势。from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.pointer_input import PointerInput actions ActionBuilder(driver) finger PointerInput(interaction.POINTER_TOUCH, finger) actions.add_action(finger.create_pointer_move(duration0, x100, y500)) actions.add_action(finger.create_pointer_down(buttoninteraction.POINTER)) actions.add_action(finger.create_pointer_move(duration250, x100, y100, origininteraction.POINTER)) actions.add_action(finger.create_pointer_up(buttoninteraction.POINTER)) actions.perform()计算坐标时确保基于当前窗口driver.get_window_size()而不是物理屏幕。问题4无线连接不稳定测试中途断开。现象测试跑着跑着就报UnknownError或连接超时。解决网络质量确保设备和电脑在同一子网Wi-Fi信号强。避免使用公共或拥挤的Wi-Fi。ADB守护进程设备休眠可能使ADB断开。在开发者选项中关闭“休眠时保持WLAN连接”为“始终”并关闭电池优化。心跳保活在测试脚本中加入定期的小操作如获取当前包名driver.current_package保持会话活跃。超时设置适当增加Appium Server的newCommandTimeout默认60秒和客户端的请求超时时间。备用方案对于关键测试考虑使用USB Hub连接多设备虽然布线麻烦但稳定性最高。5.2 性能优化建议会话复用如果测试流程是线性的尽量复用同一个Driver会话避免反复启动/关闭App和Appium Session这非常耗时。按需安装Server APK第一次连接设备时Appium会自动安装UiAutomator2 Server APK。可以提前手动安装好并在Capabilities中设置skipServerInstallation: true来跳过安装步骤。优化定位器绝对避免使用classname定位像android.widget.TextView这样通用的类几乎一定会慢且不稳定。多用resource-id它是哈希查找速度最快。减少使用XPath尤其是包含//的全局搜索和复杂的轴axis查询。使用driver.implicitly_wait设置一个合理的全局隐式等待如3-5秒而不是到处用time.sleep。关闭不必要的日志在Capabilities中设置skipLogcatCapture: true可以避免Appium持续抓取logcat日志提升网络性能。除非你需要分析日志否则可以关闭。并行测试资源配置如果进行大规模并行测试确保运行Appium Server的机器有足够的CPU和内存。每个并发的UiAutomator2 Server都会消耗设备资源。5.3 一个完整的无线自动化测试脚本框架示例最后分享一个我项目中常用的脚本框架它包含了异常处理、日志、无线设备连接检查和基本的页面对象模式思想。import logging import subprocess import time from typing import Optional from appium import webdriver from appium.options.android import UiAutomator2Options from appium.webdriver.common.appiumby import AppiumBy from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class WirelessAndroidTester: def __init__(self, device_ip: str, system_port: int, appium_server_urlhttp://localhost:4723): self.device_ip device_ip self.system_port system_port self.appium_server_url appium_server_url self.driver: Optional[webdriver.Remote] None self._ensure_wireless_connection() def _ensure_wireless_connection(self): 确保无线ADB连接正常 logger.info(f检查设备 {self.device_ip} 的连接...) result subprocess.run([adb, devices], capture_outputTrue, textTrue) if f{self.device_ip}:5555 not in result.stdout: logger.warning(f设备 {self.device_ip} 未连接尝试连接...) connect_result subprocess.run([adb, connect, f{self.device_ip}:5555], capture_outputTrue, textTrue) if connected not in connect_result.stdout: raise ConnectionError(f无法连接设备 {self.device_ip}: {connect_result.stderr}) logger.info(f设备连接成功: {connect_result.stdout.strip()}) def init_driver(self, app_path: Optional[str] None, app_package: Optional[str] None, app_activity: Optional[str] None): 初始化Appium Driver options UiAutomator2Options() options.device_name Android Wireless Device options.automation_name UiAutomator2 options.udid f{self.device_ip}:5555 options.system_port self.system_port options.new_command_timeout 300 # 5分钟无命令超时 # 优化设置 options.set_capability(skipLogcatCapture, True) options.set_capability(disableSuppressAccessibilityService, True) # 如果需要无障碍服务 if app_path: options.app app_path elif app_package and app_activity: options.app_package app_package options.app_activity app_activity else: options.no_reset True # 不重置应用数据 options.full_reset False logger.info(f正在创建Driver会话端口: {self.system_port}) self.driver webdriver.Remote(command_executorself.appium_server_url, optionsoptions) # 应用自定义Settings self.driver.update_settings({ waitForIdleTimeout: 10000, ignoreUnimportantViews: True, }) self.driver.implicitly_wait(10) # 设置隐式等待 logger.info(Driver初始化完成。) return self.driver def find_element_with_retry(self, by, value, max_retries3, timeout10): 带重试的元素查找 for i in range(max_retries): try: element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((by, value)) ) logger.debug(f元素 [{by}{value}] 在第{i1}次尝试中找到。) return element except (NoSuchElementException, TimeoutException) as e: logger.warning(f第{i1}次查找元素 [{by}{value}] 失败: {e}) if i max_retries - 1: raise time.sleep(2) # 等待后重试 # 可以在这里尝试一些恢复操作比如按返回键 # self.driver.back() def tear_down(self): 清理资源 if self.driver: try: self.driver.quit() logger.info(Driver会话已关闭。) except Exception as e: logger.error(f关闭Driver时出错: {e}) finally: self.driver None # 使用示例 if __name__ __main__: tester WirelessAndroidTester(device_ip192.168.1.100, system_port8200) try: driver tester.init_driver(app_packagecom.android.settings, app_activity.Settings) # 使用封装的方法查找元素 wifi_setting tester.find_element_with_retry( AppiumBy.ANDROID_UIAUTOMATOR, new UiSelector().textContains(网络) ) if wifi_setting: wifi_setting.click() logger.info(成功进入网络设置。) time.sleep(2) except Exception as e: logger.error(f测试执行失败: {e}) finally: tester.tear_down()这个框架提供了连接检查、Driver初始化、带重试的元素查找和资源清理是一个不错的起点。你可以根据项目需求在此基础上扩展页面对象、数据驱动、测试报告等功能。记住稳定可靠的无线自动化始于一个健壮的连接和会话管理机制。
返回列表