1. 项目概述:为什么选择 uiautomator2 + weditor 这套组合?
如果你正在为 Android 应用的 UI 自动化测试寻找一个稳定、高效且易于调试的方案,那么uiautomator2和weditor这对黄金搭档绝对值得你花时间研究。我最初接触这套工具,是因为厌倦了传统 UI 自动化框架在复杂场景下的不稳定性——要么是元素定位飘忽不定,要么是脚本维护成本高得吓人。uiautomator2作为 Google 官方UiAutomator测试框架的 Python 封装,它直接与 Android 系统的无障碍服务(Accessibility Service)和视图服务器(View Server)通信,执行速度和对原生应用的支持度都非常出色。而weditor则是一个基于 Web 的 UI 元素查看和定位工具,它解决了自动化测试中最头疼的“元素定位”问题,让你能像在浏览器里用开发者工具一样,直观地查看 App 的 UI 层级结构,并一键生成定位代码。
这套组合拳的核心价值在于:“所见即所得”的调试体验和**“稳如磐石”的执行能力**。它特别适合测试工程师、开发自测人员,甚至是想要批量操作手机完成某些重复任务的个人用户。无论你是想自动化回归测试、兼容性测试,还是模拟用户操作进行压力测试,从环境搭建到编写第一个脚本,整个过程都可以在半小时内跑通。接下来,我会带你从零开始,手把手搭建这套环境,并分享一些我踩过坑后才总结出来的实战技巧。
2. 环境搭建前的核心准备与避坑指南
在动手安装任何包之前,充分的准备工作能避免 80% 的后续问题。很多人一上来就pip install,结果遇到各种版本冲突、环境变量缺失,白白浪费几个小时。
2.1 基础环境检查清单
首先,确保你的电脑上已经安装了以下基础软件,并且版本不要太旧:
- Python 环境:
uiautomator2是一个 Python 库,因此 Python 是必须的。推荐使用 Python 3.7 到 3.10 的版本。Python 3.11 及以上版本可能存在一些第三方依赖的兼容性问题,为求稳定,建议暂时避开最新版。你可以通过命令行输入python --version或python3 --version来检查。 - ADB 工具:这是与 Android 设备通信的桥梁。
uiautomator2底层依赖 ADB 来安装应用、推送文件、执行命令。你需要将 ADB 所在目录添加到系统的环境变量PATH中。验证方法是在命令行输入adb version,如果能显示版本号则说明配置正确。 - 一台 Android 设备或模拟器:可以是真实的手机(建议 Android 5.0 (API 21) 及以上),也可以是 Android Studio 自带的模拟器(AVD)或 Genymotion 等第三方模拟器。强烈建议在初期使用模拟器,因为你可以随意折腾,不用担心把手机搞乱。
注意:对于模拟器,请确保其运行的是Google APIs或Google Play系统映像,而不是单纯的 “AOSP” 映像。因为
uiautomator2需要依赖一些 Google 服务来运行测试服务器,AOSP 映像可能缺少相关组件。
2.2 Python 虚拟环境:你的第一个“安全区”
这是我最想强调的一点:务必使用 Python 虚拟环境。直接在你的系统 Python 或全局 Python 站点包(site-packages)里安装测试工具,是灾难的开始。不同项目、不同工具对库的版本要求可能冲突,虚拟环境能为你每个项目创建一个独立的、干净的 Python 运行环境。
创建和激活虚拟环境非常简单(以项目目录uiautomator2-demo为例):
# 进入你的项目目录 cd path/to/your/uiautomator2-demo # 使用 venv 创建虚拟环境,环境文件夹名为 venv python -m venv venv # 激活虚拟环境 # 在 Windows 上: venv\Scripts\activate # 在 macOS/Linux 上: source venv/bin/activate激活后,你的命令行提示符前通常会显示(venv),表示你正在虚拟环境中操作。接下来所有的pip install命令都只会影响这个环境。
3. 核心工具安装与初始化配置
基础打牢后,我们就可以开始安装核心工具了。这个过程分为两步:安装 Python 库,以及在手机上初始化测试环境。
3.1 安装 uiautomator2 与 weditor
在激活的虚拟环境中,执行以下安装命令。我建议使用国内镜像源来加速下载,例如清华源或阿里云源。
# 使用清华镜像源安装 uiautomator2 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple uiautomator2 # 安装 weditor pip install -i https://pypi.tuna.tsinghua.edu.cn/simple weditor安装完成后,可以验证一下:
python -c "import uiautomator2 as u2; print('uiautomator2 安装成功')" python -c "import weditor; print('weditor 安装成功')"3.2 设备连接与初始化
这是最关键的一步,uiautomator2需要在你的 Android 设备上安装一个守护应用(atx-agent)和一个测试服务应用(com.github.uiautomator)。
- 连接设备:用 USB 线连接手机,并开启手机的“开发者选项”和“USB调试”模式。在命令行输入
adb devices,你应该能看到你的设备序列号,后面跟着device字样。 - 自动化初始化:
uiautomator2提供了一个非常方便的命令行工具uiautomator2 init来完成所有初始化工作。在命令行执行:
这个命令会:python -m uiautomator2 init- 自动检测已连接的设备。
- 在设备上安装
atx-agent(一个常驻后台的守护进程)。 - 通过
atx-agent安装测试服务 APK (com.github.uiautomator) 和测试应用 APK (com.github.uiautomator.test)。 - 在电脑上安装一个用于截屏和点击的辅助工具
minicap和minitouch(用于更快的操作)。
初始化常见问题与解决:
- 失败提示
Permission denied:确保手机屏幕上弹出了“允许USB调试吗?”的提示框,点击“允许”。部分手机(如小米、华为)还需要在开发者选项中开启“USB调试(安全设置)”。 - 一直卡住或下载很慢:初始化过程需要从 GitHub 下载资源,网络不稳定可能导致失败。你可以尝试使用
--mirror参数指定国内镜像:python -m uiautomator2 init --mirror https://mirrors.aliyun.com/uiautomator/ - 初始化成功后,手机桌面上出现了“ATX”和“UIAutomator”两个应用:这是正常的,请不要卸载它们。它们是测试的基础服务。
3.3 启动 weditor 进行可视化侦查
设备初始化成功后,我们就可以启动weditor来直观地查看应用界面了。在命令行输入:
python -m weditor命令执行后,它会自动在你的默认浏览器中打开一个本地网页,地址通常是http://localhost:17310。这就是weditor的界面。
- 连接设备:在 weditor 页面顶部的输入框中,输入你的设备序列号(就是
adb devices列出的那个),或者直接输入127.0.0.1:7912(这是atx-agent在设备上创建的默认服务地址,通过 USB 转发到了本地)。然后点击“连接”。 - 刷新界面:连接成功后,点击右侧的“刷新”按钮(或按
F5),当前手机的屏幕截图和完整的 UI 层级树就会显示在左侧。 - 定位元素:你可以点击截图上的任意元素,右侧的 UI 树会自动定位到对应的节点,并显示该元素的所有属性,如
resource-id,text,class,bounds等。最棒的是,你可以直接点击右侧的“复制”按钮,它就会生成对应的uiautomator2定位代码,如d(text="登录"),直接粘贴到你的脚本里就能用。
至此,你的核心测试环境已经搭建完毕。接下来,我们将深入脚本编写和实战。
4. 从零编写你的第一个自动化测试脚本
环境就绪,让我们用一个最简单的例子来感受一下uiautomator2的威力。假设我们要测试手机上的“计算器”应用,完成一个“1+2=3”的计算。
4.1 脚本基础结构
创建一个名为test_calculator.py的 Python 文件。
import uiautomator2 as u2 import time # 1. 连接设备 # 方式一:通过设备序列号连接(推荐,尤其有多台设备时) d = u2.connect('你的设备序列号') # 方式二:通过 atx-agent 地址连接(USB连接时常用) # d = u2.connect('127.0.0.1:7912') # 方式三:自动连接当前唯一设备 # d = u2.connect() print(f"设备信息: {d.info}") # 2. 启动计算器应用 # 我们需要知道计算器的包名和启动 Activity # 一个简单的方法:在手机上打开计算器,然后在 weditor 里查看顶部的 package 和 activity # 通常,系统计算器的包名可能是 `com.android.calculator2` 或 `com.google.android.calculator` app_package = 'com.android.calculator2' if d.app_current()['package'] != app_package: d.app_start(app_package) time.sleep(2) # 等待应用完全启动 # 3. 执行计算操作:1 + 2 = 3 # 假设计算器是标准布局,数字和运算符都是可点击的 View d(resourceId="com.android.calculator2:id/digit_1").click() # 点击数字 1 d(resourceId="com.android.calculator2:id/op_add").click() # 点击加号 + d(resourceId="com.android.calculator2:id/digit_2").click() # 点击数字 2 d(resourceId="com.android.calculator2:id/eq").click() # 点击等号 = # 4. 验证结果 # 获取结果框的文本。结果框的 resource-id 可能是 `com.android.calculator2:id/result` result = d(resourceId="com.android.calculator2:id/result").get_text() print(f"计算结果: {result}") expected_result = "3" if result == expected_result: print("测试通过!") else: print(f"测试失败!预期 '{expected_result}', 实际 '{result}'") # 5. 结束后,可以返回桌面或停止应用 d.press("home") # d.app_stop(app_package)代码解析与技巧:
- 连接设备:
u2.connect()是最核心的入口。使用设备序列号是最稳定的方式。 - 元素定位:
d(resourceId=“id”)是常用的定位方式,它通过 Android 控件的唯一资源 ID 来查找,精度最高。weditor就是帮你快速获取这个resourceId的神器。 - 操作 API:
.click()用于点击,.get_text()用于获取文本。uiautomator2提供了非常丰富的 API,包括滑动 (swipe)、输入 (set_text)、长按 (long_click) 等,几乎能模拟所有用户操作。 - 等待与延时:在点击操作后使用
time.sleep是一种简单的等待,但在复杂应用中可能不稳定。更好的方法是使用内置的等待,例如d(resourceId=“id”).wait(timeout=10)等待元素出现,或者d.wait_activity(“.MainActivity”, timeout=10)等待特定页面出现。
4.2 更健壮的脚本:使用 Page Object 模式
当测试用例增多时,把元素定位和操作逻辑混在一起会让代码难以维护。我们可以引入Page Object (PO) 模式,将每个页面封装成一个类,页面的元素和操作作为类的方法。
创建一个pages/calculator_page.py文件:
class CalculatorPage: def __init__(self, d): self.d = d # uiautomator2 的设备对象 # 元素定位器(Locators) _digit_1 = (u2.By.RESOURCE_ID, "com.android.calculator2:id/digit_1") _digit_2 = (u2.By.RESOURCE_ID, "com.android.calculator2:id/digit_2") _op_add = (u2.By.RESOURCE_ID, "com.android.calculator2:id/op_add") _eq = (u2.By.RESOURCE_ID, "com.android.calculator2:id/eq") _result = (u2.By.RESOURCE_ID, "com.android.calculator2:id/result") # 页面操作方法 def click_digit_1(self): self.d(*self._digit_1).click() return self # 支持链式调用 def click_digit_2(self): self.d(*self._digit_2).click() return self def click_add(self): self.d(*self._op_add).click() return self def click_equals(self): self.d(*self._eq).click() return self def get_result(self): # 这里可以增加等待结果稳定的逻辑 return self.d(*self._result).get_text()然后,主测试脚本test_calculator_po.py会变得非常清晰:
import uiautomator2 as u2 from pages.calculator_page import CalculatorPage d = u2.connect('你的设备序列号') calc_page = CalculatorPage(d) # 启动应用 d.app_start('com.android.calculator2') d.wait_activity(".Calculator", timeout=5) # 等待计算器主界面 # 执行操作:链式调用让代码更易读 calc_page.click_digit_1().click_add().click_digit_2().click_equals() # 断言结果 result = calc_page.get_result() assert result == "3", f"Expected '3', but got '{result}'" print("Page Object 模式测试通过!")使用 PO 模式后,如果计算器的界面元素 ID 改变了,你只需要在一个地方(CalculatorPage类)修改定位器,所有测试用例都会自动生效,大大提升了代码的可维护性。
5. 高级技巧与实战问题深度排查
掌握了基础之后,我们来看看如何应对更复杂的场景和那些让人头疼的“坑”。
5.1 处理动态元素与智能等待
移动应用很多元素是动态加载的(如网络列表、弹窗)。使用固定的time.sleep不是好主意,要么等不够,要么等太久。
显式等待元素出现:
# 等待最多10秒,直到“登录”按钮出现 login_btn = d(text="登录") if login_btn.wait(timeout=10): login_btn.click() else: raise TimeoutError("登录按钮未在10秒内出现")等待元素消失(常用于等待加载框):
loading = d(className="android.widget.ProgressBar") loading.wait_gone(timeout=15) # 等待加载圈圈消失,最多等15秒使用
exists进行条件判断:if d(textContains="更新").exists: d(text="忽略").click() # 如果有更新弹窗,就点忽略
5.2 处理弹窗、权限请求和特殊控件
监听并处理弹窗:
uiautomator2提供了watcher机制,可以注册一个“监视器”,当特定元素出现时自动执行操作。# 定义一个监视器,命名为 “ALLOW” d.watcher("ALLOW").when(text="允许").click() # 启动监视器,它会在后台运行,一旦发现“允许”按钮就点击 d.watcher.start() # 在需要的时候,也可以手动触发一次检查 d.watcher.run() # 测试结束后,记得关闭监视器 d.watcher.remove()这个功能对于处理应用频繁弹出的权限申请、通知弹窗等场景极其有用。
操作 WebView:对于 App 内的 H5 页面,
uiautomator2无法直接定位。你需要先切换到 WebView 上下文。- 在
weditor中,如果看到WebView组件,说明页面包含 H5。 - 在代码中,先获取所有可用的上下文(Context):
contexts = d.app_current() print(contexts) # 查看输出,通常包含 ‘NATIVE_APP’ 和 ‘WEBVIEW_包名’ - 切换到 WebView 上下文,然后就可以使用 Selenium 的 API 来操作了(需要额外安装
selenium并下载对应浏览器的驱动,如chromedriver):d.switch_to.context('WEBVIEW_com.example.app') # 现在 d 变成了一个 WebDriver 对象 d.find_element_by_css_selector(“.login-btn”).click() # 操作完后,切回原生上下文 d.switch_to.context(‘NATIVE_APP’)
- 在
5.3 稳定性提升:截图、日志与重试机制
一个健壮的测试脚本必须具备排错能力。
失败时自动截图:这是定位问题最直观的方式。
import os from datetime import datetime def take_screenshot(d, name_prefix=“error”): timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”) filename = f“{name_prefix}_{timestamp}.png” path = os.path.join(“screenshots”, filename) d.screenshot(path) print(f“截图已保存: {path}”) return path try: d(text=“不存在的按钮”).click() except Exception as e: take_screenshot(d, “element_not_found”) raise e结构化日志记录:使用 Python 标准的
logging模块,替代print。import logging logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’) logger = logging.getLogger(__name__) logger.info(“开始执行计算器测试...”) d.click(0.5, 0.5) logger.debug(“点击了屏幕中心”)操作重试装饰器:对于不稳定的操作(如网络请求后的元素加载),可以设计一个重试机制。
import time from functools import wraps def retry(times=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: if i == times - 1: raise logging.warning(f”{func.__name__} 第{i+1}次尝试失败: {e},{delay}秒后重试...”) time.sleep(delay) return wrapper return decorator @retry(times=3, delay=2) def click_login_safely(): d(text=“登录”).click()
6. 常见问题排查与解决方案实录
即使准备得再充分,实战中还是会遇到各种问题。下面是我总结的一些高频问题及其解决方法。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
uiautomator2 init失败,提示连接超时或下载失败 | 1. 设备未正确连接或 USB 调试未授权。 2. 网络问题,无法从 GitHub 下载资源。 3. 设备系统缺少必要组件(多见于纯净版模拟器)。 | 1. 执行adb devices确认设备在线且状态为device。检查手机屏幕是否点击了“允许调试”。2. 使用 --mirror参数指定国内镜像源重试。3. 尝试手动安装:先 adb install一个普通的 APK 看是否成功。对于模拟器,更换为Google APIs系统映像。 |
weditor连接成功,但刷新后界面空白或提示“无法获取层次结构” | 1. 设备上的atx-agent或测试服务未正常运行。2. 设备系统版本过高(如 Android 10+),可能存在权限限制。 3. 应用本身是游戏或使用了非常规渲染(如 Unity、Flutter)。 | 1. 在设备上手动打开“ATX”应用,查看其运行状态。重启atx-agent:adb shell /data/local/tmp/atx-agent server -d。2. 在开发者选项中,尝试开启或关闭“强制使用 GPU 渲染”、“停用 HW 叠加层”等选项。 3. 对于游戏或特殊应用, uiautomator2可能无法获取标准视图树,需考虑其他工具(如scrcpy结合图像识别)。 |
| 脚本能运行,但点击/输入等操作无效 | 1. 元素定位不准确,实际点击位置不对。 2. 元素非标准控件,无法接收点击事件。 3. 屏幕上有遮挡(如弹窗)。 | 1. 使用weditor的“点击”功能先手动测试定位是否准确。尝试使用bounds坐标定位:d.click(x, y)。2. 尝试使用 d.send_keys()进行全局输入,或使用adb shell input命令模拟。3. 运行脚本前,先通过 d.watcher处理已知弹窗。 |
| 脚本在模拟器上运行正常,在真机上失败 | 1. 真机性能差异,加载慢导致元素未出现。 2. 真机厂商定制系统(如 MIUI, EMUI)有额外的权限或后台限制。 3. 真机屏幕分辨率不同。 | 1. 在所有等待操作中增加超时时间。 2. 在手机设置中,为测试应用( ATX、UIAutomator)及被测应用开启“自启动”、“后台弹出界面”、“电池优化无限制”等权限。3. 使用相对坐标或百分比坐标,而非绝对坐标。确保脚本使用 resourceId或text等与分辨率无关的属性定位。 |
| 如何测试非前台应用(后台服务、通知栏)? | uiautomator2主要操作当前前台应用。 | 1.通知栏:使用d.open_notification()和d.open_quick_settings()。2.后台应用:使用 d.app_start(package_name)将其切换到前台。3.全局操作:使用 d.press(“home”),d.press(“back”),d.press(“recent”)等键码。 |
| 并行测试多台设备 | 需要管理多个设备会话。 | 为每台设备创建独立的u2.connect()对象,并在不同线程或进程中运行测试脚本。注意管理好每台设备的端口转发(adb -s 设备号 forward)。 |
环境搭建和基础使用只是起点,uiautomator2的潜力远不止于此。你可以将它集成到持续集成(CI)流水线中,每天自动执行回归测试;可以编写复杂的场景测试,覆盖应用的各个角落;甚至可以结合图像识别库(如opencv-python)来处理那些无法通过 UI 树定位的验证码或游戏界面。关键在于,从一个小而稳的脚本开始,逐步构建起你的自动化测试体系,把重复劳动交给机器,让自己专注于更有价值的测试设计和问题分析上。