安卓UI自动化测试:uiautomator2与weditor 0.6.4高效组合实战
1. 项目概述:为什么是uiautomator2+weditor?
如果你还在用Appium做安卓UI自动化测试,感觉像是在开一辆需要频繁保养的老爷车,那今天这个组合——Python的uiautomator2加上weditor 0.6.4——可能就是你的“新能源超跑”。我并不是说Appium不好,它在跨平台、多语言支持上依然是王者。但对于专注安卓、追求极致执行速度和开发效率的团队或个人来说,Appium那套基于WebDriver协议的架构,有时候显得有点“重”了。每次执行都要启动一个服务,通信开销也不小,在需要快速迭代、高频执行的场景下,体验并不丝滑。
uiautomator2则走了另一条路。它直接利用了安卓系统自带的UI Automator测试框架,通过Python库在PC端与手机上的atx-agent守护进程通信,实现了对手机UI的直接操控。这就好比从“远程遥控”变成了“本地直连”,指令传输的路径更短,速度自然快得多。实测下来,同样的点击、滑动操作,uiautomator2的响应速度通常比Appium快30%以上,稳定性也更好,因为它绕过了WebDriver协议可能带来的一些兼容性层。
那weditor又是什么?你可以把它理解为uiautomator2的“专属UI侦查兵”。早期我们用uiautomator2写脚本,定位元素要么靠adb shell uiautomator dump拉取XML再分析,要么凭经验写选择器,效率很低,还容易出错。weditor的出现彻底改变了这一点。它提供了一个可视化的Web界面,连接手机后,能实时显示手机屏幕,并且像Chrome开发者工具一样,让你可以点击查看任意UI元素的详细属性(resource-id, text, class, bounds等),并直接生成可用的Python定位代码。weditor 0.6.4是目前一个比较稳定且功能完善的版本,修复了不少早期版本的bug,比如对悬浮窗、动态列表的支持更好。
所以,这个组合的核心价值在于:“原生速度” + “可视化效率”。它特别适合以下场景:
- 专注安卓应用的测试开发工程师:需要编写大量稳定、快速的UI自动化用例。
- 爬虫开发者:需要对一些安卓App进行自动化操作来获取数据。
- 个人开发者或小团队:希望用最轻量、学习成本最低的方式实现自动化,快速验证想法。
- 从Appium迁移过来的老手:受够了Appium的某些慢速或不稳定,想寻求一个更纯粹的安卓解决方案。
接下来,我会带你从零开始,搞定环境搭建、核心操作,并附上我踩过无数坑才总结出来的编码避坑指南,让你能真正“告别”那些繁琐和低效。
2. 环境搭建与核心工具解析
工欲善其事,必先利其器。这一部分我们详细拆解uiautomator2和weditor的安装、初始化过程,并解释每个步骤背后的原理,确保你的基础环境坚如磐石。
2.1 Python环境与uiautomator2安装
首先,你需要一个Python环境。我个人强烈推荐使用Python 3.7到3.9之间的版本。Python 3.10及以上版本在某些依赖库的兼容性上偶尔会出点小问题,而3.6又太老。如果你电脑上已经有Anaconda,用它创建一个干净的虚拟环境是再好不过的选择,能有效避免包冲突。
# 创建并激活一个名为`u2`的虚拟环境(以conda为例) conda create -n u2 python=3.8 conda activate u2安装uiautomator2非常简单,一条pip命令搞定:
pip install uiautomator2这条命令不仅仅安装了uiautomator2这个Python库,它后续在初始化手机时,还会自动帮你处理很多事情。这里有个关键点:uiautomator2库本身不包含驱动手机的核心能力,那个能力在手机端。所以安装完Python库只是完成了第一步。
2.2 手机端初始化:atx-agent的秘密
安装好库之后,你需要用一行命令初始化你的安卓手机或模拟器:
python -m uiautomator2 init这行命令是魔法开始的地方。它会做以下几件重要的事:
- 检查ADB连接:首先确保你的电脑通过ADB连接到了手机(
adb devices能看到设备)。你需要提前安装好Android SDK Platform-Tools,并配置好环境变量。 - 推送atx-agent:将一个小型的守护进程程序
atx-agent推送到手机的/data/local/tmp/目录下。这个agent是用Go写的,非常轻量,它是PC端Python库和手机UI交互的桥梁。 - 启动并安装服务:在手机上运行atx-agent,并让它监听7912端口(默认)。同时,它还会自动在手机上安装两个辅助APK:
com.github.uiautomator(负责提供UI Automator v2的测试服务)和com.github.uiautomator.test(一个测试应用)。这两个APK是实际执行点击、滑动等操作的“手”。
注意事项:
- 手机需要开启USB调试,并且最好在弹出“允许USB调试吗?”的对话框时,勾选“始终允许”。
- 如果初始化失败,最常见的原因是网络问题(需要从GitHub下载atx-agent),或者手机系统(特别是某些深度定制的国产ROM)禁止安装来自未知来源的应用。对于后者,你需要手动在手机设置中开启“USB安装”或“通过USB安装应用”的权限。
- 模拟器:对于Android模拟器(如Android Studio自带的AVD或夜神、雷电等),初始化过程完全一样。确保模拟器已启动并通过ADB连接即可。
初始化成功后,你的手机就变成了一个等待Python指令的“智能终端”。你可以通过python -m uiautomator2 doctor命令来检查环境是否全部就绪。
2.3 weditor 0.6.4的安装与启动
weditor是一个独立的工具,也需要通过pip安装。指定版本0.6.4是为了确保稳定性。
pip install weditor==0.6.4安装完成后,启动它:
python -m weditor执行后,它会自动在你的默认浏览器中打开一个本地网页,地址通常是http://localhost:17310。这就是weditor的操作界面。
weditor界面解析:
- 连接区:顶部需要输入设备的ADB序列号(
adb devices列出的那个),或者直接输入127.0.0.1:7912(如果只有一台设备且atx-agent运行正常)。点击“Connect”即可连接。 - 实时投屏区:连接成功后,手机屏幕会实时显示在这里。你可以刷新(Refresh)来获取最新画面。
- 元素属性区:点击投屏上的任意元素,右侧会显示该元素的所有可定位属性,这是你编写脚本时最重要的信息来源。
- 代码生成区:在元素属性区下方,weditor可以直接生成用于定位该元素的Python代码片段,支持多种定位方式,可以直接复制粘贴。
实操心得: 有时候weditor连接不上,提示“无法获取截图”或“HTTP错误”。别慌,按以下步骤排查:
- 确保
python -m uiautomator2 init成功执行,手机端的atx-agent正在运行。可以尝试在命令行用adb shell ps | grep atx查看进程。- 检查防火墙是否阻止了本地端口(17310, 7912)的通信。
- 尝试重启weditor,或者换一个浏览器(Chrome/Firefox)。
- 如果使用模拟器,确保是标准的ADB连接,某些模拟器的多开器可能需要特殊的连接参数。
3. 核心API与自动化脚本编写实战
环境准备好后,我们进入实战环节。uiautomator2的API设计非常直观,学过一点Python就能快速上手。我们通过一个模拟打开“设置”应用并操作的过程来讲解。
3.1 连接设备与基础操作
首先,在Python脚本中引入库并连接设备。
import uiautomator2 as u2 # 连接设备方式一:通过ADB序列号(推荐,多设备时精准) d = u2.connect('你的设备序列号') # 例如 `emulator-5554` # 连接设备方式二:通过无线网络(初始化后,atx-agent会开启无线端口) # d = u2.connect('手机IP:7912') # 例如 `192.168.1.100:7912` # 连接设备方式三:自动连接当前唯一的设备 # d = u2.connect() # 当电脑只连接一台设备时使用连接成功后,我们就可以像操作一个对象一样操作手机。
# 1. 获取当前应用信息 print(d.info) # 打印屏幕分辨率、当前包名等信息 # 2. 屏幕操作 d.screen_on() # 点亮屏幕 d.screen_off() # 关闭屏幕 d.press("home") # 按下Home键,其他还有`back`, `power`等 # 3. 启动应用(以系统设置为例) d.app_start("com.android.settings") # 通过包名启动 # 或者通过Activity名启动(更精确) # d.app_start("com.android.settings", ".Settings") # 4. 停止应用 d.app_stop("com.android.settings") d.app_clear("com.android.settings") # 停止并清除数据 # 5. 点击与滑动(使用绝对坐标,不推荐,除非万不得已) d.click(500, 1000) # 点击坐标(500, 1000) d.swipe(500, 1500, 500, 500, 0.5) # 从(500,1500)滑动到(500,500),耗时0.5秒但绝对坐标是自动化测试的“天敌”,屏幕分辨率一变,脚本就失效了。所以,我们必须学会通过元素定位来操作。
3.2 元素定位:weditor的用武之地
这是uiautomator2最核心的部分。我们不再关心坐标,而是关心“按钮”、“文本框”这些UI元素本身。weditor在这里扮演了“侦察兵”的角色。
操作流程:
- 在weditor中连接你的手机,并打开你要测试的应用(比如“设置”)。
- 点击weditor界面上的“刷新”按钮,获取当前屏幕截图。
- 用鼠标点击截图上的目标元素,比如“WLAN”设置项。
- 查看右侧面板,你会看到类似如下的属性:
resource-id: android:id/title text: WLAN class: android.widget.TextView package: com.android.settings ... bounds: [42, 456][1038, 552] - 在“Code”区域,weditor已经生成了定位代码,比如
d(text=“WLAN”)。
现在,我们把这些定位器用到脚本里。uiautomator2支持多种定位方式,可以组合使用:
# 通过文本定位(最常用) d(text="WLAN").click() # 通过resource-id定位(最稳定,如果开发给了id的话) d(resourceId="android:id/title").click() # 通过类名定位 d(className="android.widget.TextView").click() # 通过描述(content-desc)定位,常用于无障碍或ImageView d(description="搜索").click() # 组合定位(提高精确度) d(text="WLAN", className="android.widget.TextView").click() d(resourceId="com.android.settings:id/search_bar", textContains="搜索").click() # 父子节点、兄弟节点定位(处理复杂层级) # 例如:先定位到一个父容器,再在里面找子元素 parent = d(className="android.widget.ListView") parent.child(text="蓝牙").click()获取元素信息与等待: 元素不是随时都存在的,所以等待是必须的。
# 等待元素出现(默认最多等20秒) element = d(text="WLAN").wait(timeout=10.0) # 等待10秒,返回元素对象 if element: element.click() else: print("元素未找到") # 判断元素是否存在 if d(text="WLAN").exists: d(text="WLAN").click() # 获取元素的属性 element = d(text="WLAN").get_text() print(f"元素文本是:{element}") bounds = d(text="WLAN").bounds() # 获取元素的坐标范围 center_x = (bounds[0] + bounds[2]) / 2 center_y = (bounds[1] + bounds[3]) / 23.3 复杂交互:输入、滑动与手势
除了点击,自动化测试离不开输入文本和复杂滑动。
# 1. 输入文本 - 必须先定位到输入框(EditText) d(resourceId="com.android.settings:id/search").set_text("蓝牙") # 直接设置文本,会先清空 d(resourceId="com.android.settings:id/search").send_keys("蓝牙") # 模拟键盘输入,不清空 # 2. 滑动列表 - 这是处理滚动列表的关键 # scroll.vert.forward() 垂直向前滑动(手指向上,内容向下) # scroll.vert.backward() 垂直向后滑动(手指向下,内容向上) # scroll.horiz.forward() 水平向前滑动(手指向左,内容向右) # scroll.horiz.backward() 水平向后滑动(手指向右,内容向左) # 在某个元素(如ListView)上滑动 list_view = d(className="android.widget.ListView") list_view.scroll.vert.forward() # 向下滚动一屏 list_view.scroll.vert.backward(steps=10) # 向上滚动,steps控制滚动速度/幅度 # 在整个屏幕上滑动(更常用) d.swipe(500, 1500, 500, 500, 0.5) # 从下往上滑动 d.swipe(500, 500, 500, 1500, 0.5) # 从上往下滑动 # 3. 更精确的滑动:使用元素作为参考点 # 从元素A滑动到元素B el_a = d(text="通知") el_b = d(text="显示") el_a.drag_to(el_b, duration=0.5) # 4. 手势操作:多点触控(需要坐标) d.touch.down(100, 200).move(100, 300).up() # 模拟长按拖动3.4 实战案例:自动化配置WLAN
让我们编写一个完整的迷你脚本,实现:打开设置 -> 进入WLAN -> 打开WLAN开关 -> 选择指定网络并连接(假设已知密码)。
import uiautomator2 as u2 import time d = u2.connect() # 连接设备 try: # 1. 启动设置 d.app_start("com.android.settings") time.sleep(2) # 等待应用启动 # 2. 点击进入WLAN设置 if d(text="网络和互联网").exists: d(text="网络和互联网").click() time.sleep(1) d(text="WLAN").click() else: # 有些系统设置布局不同,直接找WLAN d(text="WLAN").wait(timeout=5).click() time.sleep(1.5) # 3. 打开WLAN开关(通常是一个Switch组件) # 先判断当前状态,避免重复操作 wlan_switch = d(resourceId="com.android.settings:id/switch_widget") if wlan_switch.exists: # 获取开关状态,可能需要根据实际属性判断,这里假设通过`checked`属性 # 更通用的方法是:如果开关是“ON”文本,或者其兄弟节点有“开”字 # 这里我们简单使用点击来切换 wlan_switch.click() print("已切换WLAN开关状态") time.sleep(3) # 等待扫描网络 # 4. 在列表中找到目标网络并点击(这里以网络SSID为“MyHomeWiFi”为例) target_ssid = “MyHomeWiFi” # 方法:不断向下滑动,直到找到目标网络或滑动到底部 max_swipes = 10 for i in range(max_swipes): if d(text=target_ssid).exists: d(text=target_ssid).click() print(f"找到并点击网络: {target_ssid}") break else: # 向下滑动列表 d.swipe(500, 1200, 500, 400, 0.3) time.sleep(1) # 等待滑动后UI稳定 else: print(f"未找到网络: {target_ssid}") # 可以在这里加入截图保存,方便排查 d.screenshot(f“./not_found_{int(time.time())}.png”) # 5. 在密码输入页面输入密码并连接 # 等待密码输入框出现 password_field = d(resourceId="com.android.settings:id/password").wait(timeout=5) if password_field: password_field.set_text(“MyPassword123”) # 点击“连接”按钮,注意按钮文本可能因系统/语言而异 d(text=“连接”).click() # 或 d(text=“Connect”).click() print(“密码已输入,尝试连接”) time.sleep(5) # 等待连接过程 # 6. 验证连接成功(可选) # 可以检查是否出现“已连接”或IP地址等信息 if d(textContains=“已连接”).exists or d(textContains=“Connected”).exists: print(“WLAN连接成功!”) else: print(“WLAN连接状态未知,请手动检查。”) except Exception as e: print(f“自动化过程出现异常: {e}”) # 发生异常时截图 d.screenshot(f“./error_{int(time.time())}.png”) finally: # 脚本结束,可以按需停止应用或返回主页 # d.press(“home”) pass这个案例涵盖了启动应用、条件判断、元素等待、列表滑动、文本输入和异常处理等核心操作,是一个比较完整的流程。
4. 编码避坑指南与高级技巧
用了一段时间后,你会发现uiautomator2虽然强大,但也有一些“坑”。下面是我总结的常见问题和解决方案,以及一些提升脚本健壮性和效率的高级技巧。
4.1 元素定位的“玄学”与稳定性提升
坑1:元素属性动态变化有些App的resource-id或者text是动态生成的,每次打开都不一样。比如一个新闻列表的每一项,id可能都包含时间戳。
- 应对策略:
- 使用相对稳定的属性:优先使用
class、固定的resource-id(如果有一部分是固定的)或description。 - 使用模糊匹配:
textContains(“新闻”)、textMatches(“正则表达式”)、resourceIdMatches(“.*button.*”)。 - 使用XPath:虽然uiautomator2原生定位器很强,但复杂层级下XPath更直观。
d.xpath(‘//android.widget.TextView[@text=“登录”]’)。 - 使用兄弟/父子定位:如果目标元素不好定位,但它旁边有一个特征明显的兄弟元素,可以先用兄弟定位到那个元素,再通过
兄弟元素.sibling()或父元素.child()来定位目标。
- 使用相对稳定的属性:优先使用
坑2:元素加载慢或偶尔找不到这是UI自动化最常见的问题。
- 应对策略:
- 显式等待是王道:摒弃
time.sleep(),多用wait()方法。d(text=“确定”).wait(timeout=10).click()。 - 设置全局隐式等待:
d.implicitly_wait(10.0),但这只对d(selector).操作有效,对d(selector).exists无效。 - 重试机制:对于关键操作,可以封装一个带重试的点击函数。
- 显式等待是王道:摒弃
def click_with_retry(selector, max_retries=3): for i in range(max_retries): if selector.exists: selector.click() return True else: print(f“第{i+1}次重试,等待元素...”) time.sleep(1) print(f“元素{selector.info}在{max_retries}次重试后仍未找到”) return False click_with_retry(d(text=“稍后再说”))坑3:悬浮窗、权限弹窗遮挡自动化过程中突然弹出的系统权限弹窗或应用内悬浮广告会打断流程。
- 应对策略:
- 在关键步骤前预判:比如在启动App后,立即加入一个处理常见弹窗的逻辑。
- 使用
watcher监视器:这是uiautomator2的一个神器,可以注册一个“监视器”,当特定元素出现时自动触发操作。
# 注册一个监视器,当出现“允许”按钮时,自动点击它 d.watcher(“ALLOW_PERMISSION”).when(text=“允许”).click(text=“允许”) # 启动监视器(默认不启动) d.watcher.start() # 在需要的时候,手动运行一次监视器检查 d.watcher.run() # 注意:监视器会消耗一定资源,脚本结束后可以 d.watcher.remove(“ALLOW_PERMISSION”) 移除4.2 性能优化与脚本健壮性
技巧1:减少不必要的截图和日志weditor在连接时会持续截图,这会影响脚本执行速度。在稳定的脚本中,可以关闭weditor或断开连接。在生产环境运行脚本时,确保没有开启weditor的浏览器页面。
技巧2:使用Session管理应用状态对于需要反复测试同一个App的场景,使用session可以避免每次都用app_start冷启动,速度更快。
# 启动应用并获取session sess = d.app_start(“com.example.app”, stop=True) # stop=True表示启动前先关闭旧实例 # 后续使用sess来进行操作,上下文保持在这个应用内 sess(text=“登录”).click() # 测试结束后 sess.close()技巧3:善用adb shell命令uiautomator2可以直接执行adb命令,有些操作用命令更直接。
# 获取当前活动(Activity) current_activity = d.shell(“dumpsys window windows | grep -E ‘mCurrentFocus|mFocusedApp’”).output print(current_activity) # 模拟物理按键 d.shell(“input keyevent 3”) # 3是HOME键的keycode d.shell(“input keyevent 4”) # 4是BACK键 # 清除应用数据 d.shell(“pm clear com.example.app”)技巧4:结构化你的测试代码不要把所有代码写在一个巨大的文件里。使用Page Object模式,将每个页面的元素定位和操作封装成类,让测试用例更清晰,维护更方便。
# page/settings_page.py class SettingsPage: def __init__(self, d): self.d = d def enter_wlan(self): self.d(text=“网络和互联网”).click() self.d(text=“WLAN”).wait(timeout=5).click() return WlanPage(self.d) # 返回下一个页面的对象 # page/wlan_page.py class WlanPage: def __init__(self, d): self.d = d self.switch = d(resourceId=“com.android.settings:id/switch_widget”) def toggle_wlan(self, enable=True): current_state = self._get_switch_state() # 假设有一个方法获取状态 if (enable and not current_state) or (not enable and current_state): self.switch.click() def connect_to_network(self, ssid, password): # ... 连接网络的逻辑 pass # test_case.py from page.settings_page import SettingsPage def test_connect_wifi(): d = u2.connect() settings = SettingsPage(d) wlan_page = settings.enter_wlan() wlan_page.toggle_wlan(True) wlan_page.connect_to_network(“MyWiFi”, “password”) assert d(text=“已连接”).exists4.3 常见错误与排查清单
当你遇到脚本报错或行为不符合预期时,可以按照这个清单来排查:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
HTTPError: 500 Internal Server Error | atx-agent服务异常或未启动。 | 1. 重启手机USB调试。 2. 命令行执行 adb shell /data/local/tmp/atx-agent server -d重启服务。3. 重新 python -m uiautomator2 init。 |
UiObjectNotFoundError | 元素定位不到。 | 1.用weditor实时查看,确认元素属性是否和脚本一致。 2. 检查是否在正确的页面(Activity)。 3. 元素是否被遮挡(弹窗)?加入等待或弹窗处理。 4. 属性是否是动态的?改用模糊匹配或XPath。 |
| 脚本执行速度慢 | 1. 使用了大量time.sleep。2. weditor连接着在持续截图。 3. 网络连接不稳定(无线连接时)。 | 1. 用wait()代替固定sleep。2. 关闭weditor浏览器页面。 3. 尝试使用USB连接。 |
| 点击/输入无效 | 1. 点击坐标落在了元素不可交互区域。 2. 输入框未获取焦点。 | 1. 尝试用元素.click()代替坐标点击。2. 点击输入框后再 set_text。3. 尝试使用 元素.long_click()或元素.double_click()。 |
| 列表滑动不到底 | scroll.forward()一次滑动的距离是固定的。 | 1. 改用d.swipe()并调整滑动起点、终点和持续时间。2. 使用 while循环,直到找到目标元素或达到最大滑动次数。 |
| 在多台设备上运行不稳定 | 设备分辨率、系统版本差异。 | 1.绝对禁止使用硬编码坐标。 2. 使用相对定位(如 d(className=“ListView”).child(text=“xxx”))。3. 针对不同设备,可以准备不同的元素定位策略(如备用text)。 |
5. 与CI/CD集成及更多可能性
将uiautomator2脚本集成到持续集成/持续部署(CI/CD)流水线中,可以实现自动化测试的常态化运行。这里以Jenkins为例,给出一个最简单的思路。
基本步骤:
- 准备Slave节点:在Jenkins的Slave节点(或Master节点)上,配置好Python环境、uiautomator2、以及连接好的安卓设备(可以是实体手机长期连接,也可以是稳定的模拟器)。
- 编写启动脚本:创建一个shell脚本,用于启动模拟器(如果需要)、运行你的Python测试套件(可以使用pytest或unittest组织用例)。
- 配置Jenkins Job:
- 创建一个自由风格的软件项目。
- 在“构建”环节,选择“Execute shell”,调用你的启动脚本。
- 在“构建后操作”中,添加归档测试报告(如pytest生成的html报告)和日志的步骤。
- 处理测试结果:使用pytest插件(如pytest-html)生成美观的测试报告,并在Jenkins中展示。测试失败时,可以自动保存当时的手机截图和日志,方便排查。
更高级的用法:
- 图像识别:对于游戏或某些无法通过属性定位的控件,可以结合OpenCV等图像识别库进行辅助定位。uiautomator2本身也提供简单的
d.image.click(“button.png”)功能,但精度和性能需要评估。 - 并行测试:如果你有多台设备,可以使用
pytest-xdist等插件实现测试用例的并行分发,大幅缩短测试总时间。 - 性能监控:在自动化操作过程中,可以穿插使用
adb shell dumpsys gfxinfo或adb shell top等命令,收集应用的帧率、内存、CPU占用等性能数据。
从我个人的使用经验来看,从Appium切换到uiautomator2+weditor,最直接的感受就是“快”和“稳”。少了中间层的开销,指令执行更干脆,weditor的可视化定位也让脚本编写效率提升了不止一个档次。当然,它目前主要专注于安卓平台,如果你的项目需要覆盖iOS,Appium依然是更合适的选择。但对于安卓原生应用的自动化,无论是功能测试、数据采集还是简单的重复操作,这个组合都值得你花时间深入掌握。最后一个小建议,把常用的操作和避坑逻辑封装成你自己的工具函数库,下次新项目开始时,你会感谢现在勤于总结的自己。
