news 2026/10/20 18:40:19

QtScrcpy+Selenium+ADB联动:Web与安卓真机自动化测试方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QtScrcpy+Selenium+ADB联动:Web与安卓真机自动化测试方案

做客户端自动化测试的人,估计都遇到过这种尴尬局面:Web端脚本跑得飞快,Selenium 那边已经填完表单、点完按钮,可一到真机验证就得切回手动模式——拿起手机,划两下,点几个键,再回电脑补截图。时间一长,自动化就变成了“半自动”。后来我在某个跨端项目里试着把 QtScrcpy、Selenium、ADB 三条线彻底打通,用一套 Python 脚本同时驱动浏览器和安卓真机,QtScrcpy 负责把手机画面实时投到电脑上辅助调试与取证,整套流程才算真正跑顺。这篇就把这套方案的思路、关键参数、脚本骨架和踩坑记录完整写出来,适合正在做 App 验收、真机回归、Web 与安卓混合流程测试的团队参考。

1. 方案全景:为什么把 QtScrcpy、Selenium、ADB 绑在一起用

1.1 三个工具各管哪一段

先用最直白的话定位这三个角色,它们之间是互补关系,不是替代关系。

  • Selenium:浏览器自动化框架,管的是电脑端的 Web 页面。点击、填表、断言页面元素、拿接口数据,都在这一层。
  • ADB:安卓调试桥,管的是手机本体。安装卸载应用、模拟点击滑动、拉起指定页面、清缓存、截屏录屏,都是它说了算。
  • QtScrcpy:基于 ADB 协议的开源投屏控制工具,把安卓手机画面实时映射到电脑桌面,鼠标键盘可以直接反向操作手机。

我见过不少人把 QtScrcpy 当成普通投屏软件用,这是浪费。它底层走的是 ADB 的 screenrecord、screencap 机制和输入事件注入,本质上就是 ADB 操作的图形化客户端。在自动化测试里,它的核心价值不是抢 Selenium 或 ADB 的活,而是充当可视化监控层:脚本跑起来时,你盯着 QtScrcpy 窗口就能确认手机上每一步到底点没点对,出问题当场肉眼定位,排查效率高得多。

1.2 这套组合解决的三个痛点

先说说我在真实项目里反复遇到的三类问题,你就明白为什么非要这么组合。

痛点一是真机验证靠肉眼。Web 端自动化断言很成熟,元素出现没出现、文本对不对,脚本一查便知。但 App 端登录成功没有、有没有系统弹窗拦截、页面渲染完没完,脚本看不见。有了 QtScrcpy 实时画面,人可以在关键节点介入确认,必要时候还能手动补两下操作再让脚本继续。

痛点二是操作链断裂。很多业务是“电脑上发起、手机上确认”的跨端流程,典型如扫码登录、短信验证、订单确认。用户先对着电脑屏幕扫码,弹出来的却是在手机上点确认。只靠 Selenium 只能处理电脑侧,只靠 ADB 又拿不到网页状态,两头各跑各的覆盖不了真实业务。把 Selenium 和 ADB 编进同一个测试流程,才有办法模拟完整用户链路。

痛点三是设备初始化重复劳动。每个测试用例跑之前,都要清数据、装包、解锁屏幕、拉起指定页面。这些动作用 ADB 写成批处理脚本,几秒钟完成且稳定复现,手工操作至少几十秒,还容易漏步骤。配上 QtScrcpy 的窗口看着执行过程,比黑盒执行踏实。

2. 环境准备:先把设备和工具链打通

2.1 软件清单与版本匹配建议

在动手写脚本前,先确认软件版本,很多怪问题都出在版本不匹配上。我整理了一张常用的版本参考表,照这个选型能少踩一半坑。

软件建议版本说明
Python3.10 及以上3.7 也行,但 f-string 调试体验和第三方库兼容性差一些
Selenium4.x 系列4.0 之后 API 更干净,自带不少等待工具
QtScrcpy2.x 系列2.1 以后对高刷屏支持更稳
Android SDK Platform-Tools33.0.0 及以上版本太老容易漏命令参数
ChromeDriver与 Chrome 版本严格对应用 Selenium 管浏览器时最容易翻车的地方

Python 环境建议用虚拟环境管理,避免多个项目互相污染。Selenium 4 的库安装很简单,一条 pip 命令搞定,但 ChromeDriver 要特别注意:它跟 Chrome 浏览器版本必须严格匹配,主版本号不一致浏览器直接拒绝启动。我的习惯是先在终端跑chromedriver --version 确认可执行,再写 WebDriver 初始化代码。

2.2 ADB 连接与 QtScrcpy 稳定显示的关键配置

设备连接这块,我的建议是脚本调试阶段一律用 USB 线,调试稳定后再考虑无线方案。USB 连接出问题九成出在三个方面:线不行、电脑 USB 口供电不足、设备没解锁信任。

连上之后先用adb devices 确认设备状态。正常情况下状态列显示 device,如果是 unauthorized 说明手机上的 USB 调试授权弹窗没点确认;如果是 offline 就拔线重插一次,或者执行 adb kill-server 再 adb start-server 重启 ADB 服务。

解锁屏幕这步经常被忽略。手机锁屏状态下,很多命令执行了也没效果。保险做法是设备连上后先跑:

adb shell wm dismiss-keyguard adb shell input keyevent 82

这两条能让大部分手机进入可用状态,keyevent 82 对应菜单键,能把锁屏界面往上一推。

QtScrcpy 启动参数别用默认值硬跑。我常用的启动命令是:

scrcpy --max-fps 30 --bit-rate 4M --always-on-top

限定帧率到 30 帧、码率到 4M,画面足够看清操作反馈,也不会把 CPU 吃满导致电脑卡顿;--always-on-top 让窗口随时置顶,调试时盯着脚本输出和手机画面都方便。多台设备同时接电脑时,必须带设备序列号启动,否则工具不知道该投哪一台:

scrcpy --serial 设备序列号 --max-fps 30

3. 核心脚本设计:Selenium 与 ADB 命令的编排方法

3.1 整体流程与数据桥接

这套方案在代码层面的核心,是把 Selenium 和 ADB 当成两个可调用的服务,而不是两个独立程序。脚本主流程是这样的:

  1. Selenium 层完成网页操作,比如打开注册页、填邮箱、提交表单。
  2. 网页操作产生结果(二维码图片、验证码文本、订单号),把结果保存成文件或变量。
  3. DeviceController 层用 ADB 在真机上执行对应动作,比如拉起 App、点击确认按钮、截图保存。
  4. QtScrcpy 作为旁观窗口,脚本运行时人工确认每一步动作是否符合预期。
  5. 最后统一收报告,把 Web 截图、真机截图、日志归档到一起。

关键在于第 2 步的数据桥接。我在 Python 里用一个 DeviceController 类把设备操作全部收敛起来,脚本主体只跟这个类打交道,不直接拼 ADB 命令字符串,维护起来清爽很多。

3.2 用 Python 封装高频 ADB 指令集

我封装的 DeviceController 类,核心方法就五个:tap、swipe、text、screencap、start_app。代码骨架如下:

import subprocess import time import os class DeviceController: def __init__(self, serial=None): self.serial = serial self.base_cmd = "adb" if serial: self.base_cmd = f"adb -s {serial}" def execute(self, args, timeout=10): cmd = f"{self.base_cmd} {args}" result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) if result.returncode != 0: raise RuntimeError(f"ADB 命令失败: {cmd}\n{result.stderr}") return result.stdout.strip() def tap(self, x, y): self.execute(f"shell input tap {int(x)} {int(y)}") time.sleep(1) def swipe(self, x1, y1, x2, y2, duration=300): self.execute(f"shell input swipe {x1} {y1} {x2} {y2} {duration}") time.sleep(1) def screencap(self, save_path): remote_path = "/sdcard/tmp_screen.png" self.execute(f"shell screencap -p {remote_path}") self.execute(f"pull {remote_path} {save_path}") os.remove(remote_path) if os.path.exists(remote_path) else None def start_app(self, package_name, activity_name=None): if activity_name: self.execute(f"shell am start -n {package_name}/{activity_name}") else: self.execute(f"shell monkey -p {package_name} -c android.intent.category.LAUNCHER 1") time.sleep(3)

注意几个细节。第一,每条 ADB 命令都必须设超时,执行卡住时不至于让整个用例挂死。第二,tap 和 swipe 结束之后固定 sleep 一下,让界面动画有时间跑完,不然下一步操作容易打在旧界面上。第三,screencap 先截到手机 /sdcard 再 pull 回电脑,直接 screencap 到电脑路径在部分机型上不生效。

3.3 跨端参数传递的关键点

数据怎么从 Selenium 手上传给 ADB 手,这步是整套方案最容易写乱的地方。我的做法分成三类:

页面文本类:Selenium 通过 text 属性取回来,直接做变量传给 DeviceController 的 text 方法,用来在 App 端输入同样内容。但要注意 adb shell input text 对中文支持很差,后面常见问题部分专门讲。

图片文件类:网页产生的二维码、验证码,用 Selenium 截图保存成 PNG,再把路径传给 ADB 去推送或比对。

坐标位置类:Web 端拿不到手机端元素坐标,手机端坐标必须靠 uiautomator dump 拿。执行下面这行拿到当前界面的控件树:

adb shell uiautomator dump /sdcard/ui.xml adb pull /sdcard/ui.xml ./ui.xml

解析这个 XML 找到目标控件的 bounds 属性,比如“激活通知”按钮的 bounds 是 [200,800][400,880],中心点就是 ((200+400)/2, (800+880)/2) = (300, 840),拿着这个坐标再去 tap。这套方法比写死坐标通用得多,因为它在每次执行时动态读取当前界面布局。

4. 实操演示:一个完整的自动注册测试用例

4.1 测试场景设定与断言策略

为了把方案讲透,我设定一个常见场景:某产品同时有 Web 端注册入口和 App 端激活入口,用户注册后需要在手机 App 上点击“激活通知”才算完成整个流程。自动化要做的就是模拟这条完整链路。

断言分两层。Web 层的断言用 Selenium 原生能力:注册成功后的跳转 URL、欢迎语文本、隐藏的错误提示。真机层的断言不能只看截图——静态截图没法自动判断对错,我用的办法是再次执行uiautomator dump,检查激活通知是否变成了“已激活”,或者目标页面是否出现了指定文本节点。这个断言比截图可靠,也好归档。

4.2 脚本分步实现与注释解析

完整流程大概长这样:

from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from xml.etree import ElementTree import time def get_center_from_xml(xml_path, target_text): tree = ElementTree.parse(xml_path) for node in tree.iter("node"): if node.get("text") == target_text: bounds = node.get("bounds") # bounds 形如 [200,800][400,880] import re nums = list(map(int, re.findall(r"\d+", bounds))) return ((nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2) return None # 初始化 Selenium driver = webdriver.Chrome() wait = WebDriverWait(driver, 15) device = DeviceController() def test_register_flow(): try: # 第一步:Selenium 完成 Web 注册 driver.get("http://example.com/demo-register") wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("test@example.com") driver.find_element(By.ID, "password").send_keys("Abc123456") driver.find_element(By.ID, "submit_btn").click() # 等待跳转到成功页 wait.until(EC.url_contains("register_success")) success_text = driver.find_element(By.CLASS_NAME, "welcome_msg").text assert "注册成功" in success_text # 第二步:ADB 拉起 App device.execute("shell am force-stop com.example.app") device.start_app("com.example.app", "com.example.app.MainActivity") # 第三步:读取界面控件树,找到激活按钮 device.execute("shell uiautomator dump /sdcard/ui.xml") device.execute("pull /sdcard/ui.xml ./ui.xml") click_point = get_center_from_xml("./ui.xml", "激活通知") assert click_point is not None, "激活按钮未找到" # 第四步:点击激活按钮 device.tap(click_point[0], click_point[1]) # 第五步:真机断言 time.sleep(2) device.execute("shell uiautomator dump /sdcard/ui.xml") device.execute("pull /sdcard/ui.xml ./ui.xml") final_point = get_center_from_xml("./ui.xml", "已激活") assert final_point is not None, "激活状态未更新" # 第六步:真机截图取证 device.screencap(f"./reports/register_activated.png") finally: driver.quit()

这个脚本看着不长,但该有的环节都有了。核心思路上,Web 和真机不串行等待对方空转,而是在关键节点主动拉取状态,确保数据同步。第五步断言用“已激活”节点是否存在做判断,比比对像素靠谱得多,也避免了按钮文案变化导致截图比对误判。

4.3 失败重试与截图取证

自动化跑起来之后,关键是失败时能留下足够信息。我的策略很简单:每个关键步骤包一层 try,失败后立即执行设备截屏和当前页面 HTML 保存,文件名带上用例名和时间戳。

def safe_step(step_name, func): try: return func() except Exception: ts = time.strftime("%Y%m%d_%H%M%S") driver.save_screenshot(f"./reports/{step_name}_{ts}_web.png") device.screencap(f"./reports/{step_name}_{ts}_device.png") with open(f"./reports/{step_name}_{ts}.html", "w", encoding="utf-8") as f: f.write(driver.page_source) raise

真机侧有时候点击没生效,不是脚本错了,是 App 启动动画还没结束。这种偶发问题我习惯加简单重试:拿到坐标点之后,tap 一次,等两秒,再 dump 一次界面,如果目标状态没出现,再 tap 一次,最多三次。常规的时间等待是“等等看”,重试机制是“没等到就再看一次”,后者在真机上稳定得多。

5. 常见问题与排查实录

5.1 设备频繁断连、adb devices 时有时无

这是真机测试最多见的毛病。优先排查线材:很多便宜的 USB 线只支持充电,数据线识别不出来或者跑着跑着就掉线。其次是供电,电脑前面的 USB 口供电不稳,换到主机背面或者用带供电的 HUB 会好很多。还有一个容易被忽略的是手机锁屏休眠,执行长时间用例时屏幕一灭,部分机型会断开 ADB 连接。解决方式是开发者选项里把“充电时屏幕不休眠”开开,配上 adb shell svc power stayon true 临时保持唤醒。

碰到 adb devices 空列表时,别急着换线,先执行 adb kill-server && adb start-server 重开服务,很多临时故障都能恢复。如果还是不行,拔掉手机重新授权一次。

5.2 Selenium 与 ADB 时序不同步

Web 页面跳转慢、App 启动慢、动画没播完,脚本照固定 sleep 时间执行,很容易出现页面还没就绪就点了下一个按钮。这个问题的根子是“时间等待”写死了,解决办法只有一条:把 sleep 换成轮询等待。

Web 端用 WebDriverWait,这是 Selenium 自带的标准做法。真机端就自己写轮询函数,每 0.5 秒 dump 一次界面,检查目标文本或控件出现才继续,最多等 15 秒超时。这样不管机器多卡,脚本总能在正确时机执行下一步。

5.3 点击坐标失效、界面位置变化

不同分辨率手机、不同屏幕尺寸,同一按钮的坐标完全不一样。写死坐标的最多在测试机上能跑,换台机型就崩。两个成熟解法:一是通过 uiautomator dump 的 bounds 动态计算目标中心点,这个方法前面代码里已经实现了;二是拿到屏幕尺寸按比例换算:

adb shell wm size

比如测试机分辨率是 1080x2400,按钮在 540x1200,目标机分辨率 1440x3200,按钮坐标大约就是 720x1600。这个转换系数在分辨率比例一致时够用,但布局差异大的 App 还是得靠控件树定位。能用控件树定位就不建议用比例法,代码更清晰、鲁棒性也高。

5.4 高频命令与常用参数速查表

操作命令或参数备注
查看设备adb devices状态为 device 才可用
点亮屏幕adb shell input keyevent 224224 是 WAKEUP
解锁adb shell wm dismiss-keyguard有些机型需要先点亮
拉起应用adb shell monkey -p 包名 -c android.intent.category.LAUNCHER 1不会指定具体 Activity 时首选
输入英文/数字adb shell input text "abc123"中文输入不稳定
模拟点击adb shell input tap x y坐标单位像素
模拟滑动adb shell input swipe x1 y1 x2 y2 300最后是时长毫秒
截屏adb shell screencap -p /sdcard/s.png配合 adb pull 拉回本地
录屏adb shell screenrecord --time-limit 10 /sdcard/v.mp4默认输出 mp4
查看当前界面控件树adb shell uiautomator dump /sdcard/ui.xml再 pull 回电脑解析
清应用数据adb shell pm clear 包名测试前必做
强制停止 Appadb shell am force-stop 包名配合清数据

QtScrcpy 三个顺手快捷键也记录一下:全屏切换是 Ctrl+F,旋转画面是 Ctrl+R,直接剪贴板复制到设备是 Ctrl+V,调试时效率能提不少。

6. 实操心得与后续扩展

6.1 我在实际项目中踩过的坑

这套方案我跑了两个迭代版本,积累了一些常规文档里看不到的经验,逐个分享。

第一,adb shell input text 对中文输入的支持非常差,直接输入中文经常吞字符。我后来用剪贴板方式绕开:电脑端先把中文文本写入本地剪贴板,再执行 adb shell ime enable 切到 ADBKeyboard 输入法,最后通过广播把剪贴板内容送进焦点输入框。没有 ADBKeyboard 环境时,退一步用英文标识或编号代替中文输入,测试数据设计上就要规避。

第二,QtScrcpy 画面与设备实际显示存在肉眼难以察觉的延迟。我踩过一次,脚本在手机上点了“下一步”,QtScrcpy 画面还停在上一页,我以为是点击没生效,连续手动点了几次造成重复提交。从那以后我明确一条规矩:脚本判断永远以 uiautomator dump 的控件树为准,QuScrcpy 只做人工监控,不做断言依据。

第三,Selenium 和 ADB 偶尔会打架。有次脚本跑着跑着报 adb 端口被占用,排查半天发现是 WebDriver 服务抢占了一个端口号,ADB 的 5037 也凑热闹。解决方式是先把固定端口统一规划:WebDriver 用 9515 或自定义,ADB 保持 5037,不用默认随机端口,冲突概率大减。

第四,用例收尾环境清理。注册类用例如果不在 teardown 里清数据,第二次跑就会遇到账号已注册之类的冲突。我固定在每个用例结束前执行adb shell pm clear 包名,再删除 Web 端测试账号记录,环境回到干净状态,重复执行才稳定。

6.2 从脚本到自动化平台

如果项目已经在跑 CI,这套脚本可以直接升级成自动化平台的一部分。Python 侧的 pytest 管理用例,Allure 生成测试报告,配合 Jenkins 或类似工具定时跑回归,报告里把 Web 截图和真机截图挂在一起,问题归属一目了然。设备管理也可以做一层,用一个 JSON 文件记录每个测试机的序列号、型号、分辨率,多机并行时按用例标签分配设备,资源利用率高不少。

后续还能扩展的方向,一是 OCR 识别真机截图,直接从像素里捡文案,比 uiautomator dump 拿到更多信息;二是把 QtScrcpy 的录屏能力用起来,跑完的用例自动生成一段真机操作视频,作为 bug 复现证据比静态截图有力得多。这两块我团队已经在做,跑通后再单独写一篇。

最后再分享一个小心得:这套方案刚开始搭的时候,别急着把所有用例都自动化。把一条最核心的跨端流程跑通,比如注册激活,让团队看到脚本自己点完网页又点手机、最后自动归档截图的完整过程,后面推广阻力会小很多。工具组合是固定的,真正值钱的是把流程编排顺的手感,这东西只能靠一次一次跑用例练出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:40:08

播放量500不是限流:读懂短视频推荐机制,打破流量瓶颈

1. 这一期先把它当回事:500播放量真的不是限流短视频做到第三四个月的时候,最崩溃的一句话通常是:"我是不是被限流了?"尤其是认认真真剪了三个小时的视频,发出去两个小时,打开后台一看&#xff0…

作者头像 李华
网站建设 2026/10/20 18:39:52

轻量级前端方案实践:打造高效可维护的移动端H5项目

1. 从"rea"这个代号说起:一个轻量级前端方案的诞生去年下半年,我在做一个移动端H5项目时,被团队内部一个内部代号为"rea"的小工具帮了大忙。这个代号全称是"Reactive Easy Application",翻译过来就…

作者头像 李华
网站建设 2026/10/20 18:39:27

技能管理:从分类到组合,构建可运营的个人技能系统

1. 从“skills”这个标题说起:一个被严重低估的硬核话题“skills”这个词,单独拎出来看,信息量几乎为零。它不像“分布式事务”或者“图像分割”那样自带技术边界,也不像“副业”或者“效率工具”那样自带场景。但恰恰是这种模糊性…

作者头像 李华
网站建设 2026/10/20 18:38:51

预编译pdfium静态库接入实战:CMake配置、渲染与文本提取避坑指南

简介:这份资源是已编译完成的PDFium静态库,面向需要在C/C项目中集成PDF解析、渲染与打印能力的开发者,尤其适合不愿从源码编译、希望快速接入PDF处理功能的工程团队。压缩包共45个文件,约2.92MB,以24个h头文件为核心&a…

作者头像 李华
网站建设 2026/10/20 18:38:49

Postman接口测试参数化全攻略:变量体系、数据驱动与传参避坑

开头部分:做接口测试的同学,十有八九都会遇到同一个坎:接口本身测通了,但一到自动化就跑不起来。不是断言写错,也不是环境配错,而是用例里写死了数据——登录接口拿到的token是死的,查询接口的I…

作者头像 李华
网站建设 2026/10/20 18:38:39

CTF Python逆向实战:从ez_python题解析字节码反编译与混淆对抗

1. 从一道CTF题看Python逆向的典型套路 "ez_python"这个标题在CTF圈子里几乎自带一种信号——出题人想让你觉得简单,但实际动手之后往往会发现,坑都埋在你看不见的地方。第一届"长城杯"网络安全大赛的这道题,名字虽然叫&…

作者头像 李华