QtScrcpy+Selenium+ADB构建安卓混合应用自动化测试框架
1. 项目概述:为什么需要QtScrcpy+Selenium+ADB的组合?
如果你是一名移动端测试工程师,或者正在尝试将PC上的自动化测试能力延伸到安卓设备,那么你很可能已经对Selenium和ADB(Android Debug Bridge)这两个名字耳熟能详。Selenium是Web自动化测试的王者,而ADB则是连接PC与安卓设备的瑞士军刀。但一个核心痛点始终存在:如何让运行在PC浏览器上的Selenium脚本,去精准地操控一台真实安卓设备屏幕上的应用界面?尤其是当这个应用是混合应用(Hybrid App)或者需要测试其与手机原生功能的交互时。
传统的纯Selenium方案对此无能为力,因为它只能操作浏览器内的DOM元素。而纯ADB命令(如input tap,input text)虽然能模拟点击和输入,但定位元素极其笨拙,严重依赖预先获取的屏幕坐标,一旦UI布局变化,脚本就全盘崩溃。这时,一个能将手机屏幕实时投屏到PC,并能获取屏幕图像进行视觉分析的工具,就成了连接两者的关键桥梁。这就是QtScrcpy的价值所在。
QtScrcpy不仅仅是一个高清、低延迟的安卓投屏工具。它的核心优势在于开源和可编程性。通过其提供的接口,我们可以实时捕获手机屏幕帧,结合图像处理技术(如OpenCV模板匹配、OCR)来定位UI元素,再驱动ADB执行对应的触摸、滑动等操作。而Selenium则负责处理应用内的WebView部分。三者结合,形成了一套覆盖从设备连接、屏幕感知、元素定位到动作执行的完整自动化测试闭环。这套方案尤其适合测试混合应用、需要验证UI渲染效果、或者进行跨应用流程的自动化场景。接下来,我将拆解如何将这三者无缝集成,构建一个稳定、高效的自动化测试框架。
2. 环境搭建与核心工具链配置
工欲善其事,必先利其器。一个稳定的环境是自动化测试的基石。这部分我会详细说明每个工具的安装、配置以及版本兼容性注意事项,确保你的环境一次配成,避免后续踩坑。
2.1 ADB环境深度配置
ADB是整个工具链的底层通信基础。很多人以为安装Android Studio就万事大吉,但为了自动化脚本的稳定和可移植性,我强烈建议进行独立配置。
安装与路径配置:
- 独立SDK Platform-Tools下载:前往安卓开发者官网,直接下载
platform-tools压缩包。这比安装完整的Android Studio更轻量,也更容易管理版本。 - 系统环境变量配置:解压后,将
platform-tools目录的路径(例如D:\android\platform-tools)添加到系统的PATH环境变量中。这是关键一步,确保在任意命令行窗口都能直接调用adb命令。 - 验证安装:打开新的命令行终端(CMD或PowerShell),输入
adb version。正确输出类似Android Debug Bridge version 1.0.41的信息即表示成功。
设备连接与授权:
- USB连接:使用数据线连接安卓手机和电脑。在手机上弹出的“是否允许USB调试?”对话框中,务必勾选“始终允许”,并点击确定。这是自动化脚本能无人值守运行的前提。
- 无线连接(可选但推荐):对于需要长期连接或连接多台设备的情况,无线ADB更灵活。
- 首先确保手机和电脑在同一局域网。
- 通过USB线执行一次:
adb tcpip 5555。这个命令将设备的ADB守护进程切换到TCP/IP模式,并监听5555端口。 - 拔掉USB线,执行:
adb connect 手机IP地址:5555(例如adb connect 192.168.1.100:5555)。 - 连接成功后,后续即可无线操作。注意:设备重启后可能需要重新执行
adb tcpip 5555步骤。
实操心得:在团队协作或CI/CD环境中,强烈建议使用无线ADB。它可以避免因USB端口变动、线缆松动导致设备序列号变化,从而影响脚本稳定性。可以将
adb connect命令写入脚本的初始化阶段。
2.2 QtScrcpy的编译与关键功能启用
直接从GitHub下载QtScrcpy的发布版可执行文件是最快的方式。但如果你想启用一些高级功能(如修改投屏分辨率、帧率)或进行二次开发,则需要从源码编译。
Windows下编译要点:
- 环境准备:安装Qt Creator(建议5.15或以上版本)和对应的Qt库。同时需要安装CMake和Visual Studio(作为C++编译器)。
- 获取源码:
git clone https://github.com/barry-ran/QtScrcpy.git - 使用CMake配置:在Qt Creator中打开项目,使用CMake进行构建。重点在于配置
CMakeLists.txt中的选项,例如可以设置默认的投屏码率(-b 8M)和最大尺寸(-m 1920)。 - 解决依赖:编译过程中可能会缺少
libusb等库,需要根据错误提示手动下载并放置到正确目录。
对于大多数自动化测试场景,我们主要利用QtScrcpy的两个核心能力:
- 屏幕图像流获取:通过其提供的
--record参数或直接调用其底层接口,将屏幕帧保存为图像或视频流,供后续图像分析使用。 - 键鼠事件转发:QtScrcpy可以将PC端的鼠标点击和键盘输入事件转发到手机。但在自动化中,我们更倾向于用ADB命令来模拟这些事件,因为ADB命令更易于脚本化且不依赖前台窗口焦点。
2.3 Selenium与浏览器驱动匹配
由于我们的目标是测试混合应用中的WebView部分,因此需要配置Selenium来控制PC上的浏览器,以访问或调试手机WebView的内容。
- 安装Selenium库:通过pip安装Python版本的Selenium:
pip install selenium。 - 下载浏览器驱动:
- Chrome:下载与你的Chrome浏览器版本完全匹配的
chromedriver。可以在Chrome的“关于”页面查看版本号,然后去ChromeDriver官网下载。 - 其他浏览器:如Firefox需下载
geckodriver。
- Chrome:下载与你的Chrome浏览器版本完全匹配的
- 驱动放置:将下载的驱动可执行文件放在一个目录下,并将该目录添加到系统
PATH,或者直接在Selenium代码中指定驱动路径。
版本匹配是重中之重。浏览器自动升级后,驱动也必须同步更新,否则Selenium会报错。一个实用的技巧是在脚本初始化时,加入版本检查逻辑,或者使用webdriver-manager这类第三方库自动管理驱动版本。
3. 核心架构设计:打通三者的协作链路
理解了每个工具的作用后,我们需要设计一个清晰的架构,让它们协同工作。核心思路是:以图像为媒介,以ADB为执行器,以Selenium为Web专家。
3.1 基于图像识别的元素定位策略
这是替代Appium等框架中“控件树”定位的核心方法。我们无法直接获取安卓原生控件的resource-id或xpath,但可以通过“看”屏幕来定位。
屏幕截图获取:
- 方法A(ADB命令):
adb exec-out screencap -p > screen.png。这是最直接的方法,但速度稍慢。 - 方法B(QtScrcpy接口):通过解析QtScrcpy的视频流,实时截取帧。这种方法更快,可以实现近乎实时的屏幕监控,但对编程能力要求较高。通常可以结合
opencv库直接从视频流中读取帧。
- 方法A(ADB命令):
元素定位技术:
- 模板匹配(Template Matching):预先截取需要点击的按钮、图标等UI元素的图片作为“模板”。在实时屏幕截图中,使用OpenCV的
cv2.matchTemplate函数寻找最佳匹配位置。这种方法对于图标、固定样式的按钮非常有效。
import cv2 import numpy as np def find_element_by_template(screen_path, template_path, threshold=0.8): screen = cv2.imread(screen_path, 0) # 灰度图 template = cv2.imread(template_path, 0) result = cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) if max_val >= threshold: # 计算元素中心点坐标 h, w = template.shape center_x = max_loc[0] + w // 2 center_y = max_loc[1] + h // 2 return (center_x, center_y) else: return None- OCR文字识别:当需要定位带有特定文字的控件(如“登录”、“提交”按钮)时,可以使用Tesseract等OCR库识别屏幕上的文字,然后根据文字位置反推控件坐标。
pytesseract库是不错的选择。 - 特征匹配(Feature Matching):对于形状可能略有变化但特征明显的元素,可以使用SIFT、ORB等算法进行特征点匹配。这比模板匹配更健壮,但计算量更大。
- 模板匹配(Template Matching):预先截取需要点击的按钮、图标等UI元素的图片作为“模板”。在实时屏幕截图中,使用OpenCV的
3.2 动作执行:ADB命令的精准调用
一旦通过图像识别获得了目标元素的屏幕坐标(x, y),就可以通过ADB命令来模拟用户交互。
- 点击:
adb shell input tap x y - 长按:
adb shell input swipe x y x y duration(起始和结束坐标相同,通过duration参数控制长按时间,单位毫秒)。 - 滑动:
adb shell input swipe x1 y1 x2 y2 [duration] - 文本输入:
adb shell input text "your_text_here"。注意:此命令无法输入中文和特殊字符。对于复杂输入,可以结合剪贴板:adb shell am broadcast -a clipper.set -e text "中文内容",然后模拟粘贴操作。 - 按键事件:如返回键
adb shell input keyevent 4,Home键adb shell input keyevent 3。
3.3 Selenium处理WebView的桥接方法
对于混合应用,应用内的网页部分(WebView)需要由Selenium来处理。关键是如何让Selenium“进入”手机上的WebView上下文。
- 启用WebView调试:在安卓应用中,开发者需要启用WebView的调试功能(通常是在WebView类中调用
setWebContentsDebuggingEnabled(true))。对于测试自己的应用,这可以做到;对于第三方应用,则取决于其是否开启。 - 发现WebView:通过ADB命令
adb shell cat /proc/net/unix或检查chrome://inspect页面(对于Chrome内核的WebView),可以找到WebView的调试端口。 - 远程连接:Selenium的
webdriver.Remote可以连接到一个远程的WebDriver服务。我们需要在PC上启动一个代理(如chromedriver在特定端口),然后通过ADB端口转发,将手机上的WebView调试端口映射到PC。
然后,Selenium就可以通过adb forward tcp:9222 localabstract:webview_devtools_remote_<进程ID>localhost:9222连接到这个WebView,像操作普通网页一样进行自动化测试。
架构流程图(文字描述):
- 脚本启动,通过ADB连接设备,并启动QtScrcpy获取屏幕流。
- 进入测试用例:判断当前界面是原生页面还是WebView。
- 若是原生/混合应用原生部分:通过图像识别(模板匹配/OCR)定位目标元素,计算坐标,调用ADB命令执行操作。
- 若是WebView内容:通过ADB端口转发,建立Selenium WebDriver连接,使用Selenium的API(find_element_by_id, xpath等)定位元素并操作。
- 操作后,通过图像识别或Selenium的预期条件,判断操作结果(如新页面出现、Toast提示等),进行断言。
- 循环执行,直到用例结束。
4. 实战:构建一个完整的自动化测试用例
让我们以一个经典的场景为例:测试一个电商混合应用,完成从启动、登录(原生界面)、搜索商品(WebView)、加入购物车(原生界面)的流程。
4.1 用例设计与初始化
首先,我们设计一个Python的测试类,并完成初始化工作。
import subprocess import time import cv2 import pytesseract 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 class HybridAppTest: def __init__(self, device_serial=None): self.device_serial = device_serial self.adb_cmd = 'adb' if not device_serial else f'adb -s {device_serial}' # 启动QtScrcpy (这里假设使用命令行启动) self.scrcpy_process = subprocess.Popen(['scrcpy', '-s', device_serial] if device_serial else ['scrcpy']) time.sleep(3) # 等待投屏稳定 # 初始化OCR引擎(如果需要) pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # Selenium WebDriver稍后按需初始化 def adb_shell(self, cmd): """执行ADB shell命令""" full_cmd = f'{self.adb_cmd} shell {cmd}' return subprocess.run(full_cmd, shell=True, capture_output=True, text=True) def take_screenshot(self, filename='current_screen.png'): """通过ADB截取屏幕""" subprocess.run(f'{self.adb_cmd} exec-out screencap -p > {filename}', shell=True) return filename4.2 步骤一:启动应用与原生登录
假设应用主Activity为com.example.shop/.MainActivity,登录按钮是一个带有“登录”文字的TextView。
def test_login(self): # 1. 启动应用 self.adb_shell('am start -n com.example.shop/.MainActivity') time.sleep(5) # 等待应用启动 # 2. 图像识别定位登录按钮 screen = self.take_screenshot() # 方法A: 使用预先准备好的“登录”按钮模板图片进行匹配 login_button_pos = self.find_by_template(screen, 'template_login_button.png') if login_button_pos: self.adb_shell(f'input tap {login_button_pos[0]} {login_button_pos[1]}') else: # 方法B: 使用OCR识别“登录”文字 img = cv2.imread(screen) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) data = pytesseract.image_to_data(gray, output_type=pytesseract.Output.DICT) for i, text in enumerate(data['text']): if '登录' in text: x = data['left'][i] + data['width'][i] // 2 y = data['top'][i] + data['height'][i] // 2 self.adb_shell(f'input tap {x} {y}') break time.sleep(2) # 3. 在登录界面输入用户名密码(假设是原生输入框) self.adb_shell('input text testuser') self.adb_shell('input keyevent 66') # 模拟Tab键或下一步,取决于UI self.adb_shell('input text password123') # 4. 点击“确定登录”按钮(同样用图像识别) # ... 省略定位和点击代码 print("登录步骤完成")4.3 步骤二:进入WebView商品搜索
登录后,应用跳转到首页,其中包含一个WebView渲染的商品搜索页。
def test_search_in_webview(self): # 1. 首先需要找到并连接WebView # 获取应用进程ID result = self.adb_shell('ps | grep com.example.shop') pid = result.stdout.split()[1] # 简化处理,实际需解析 # 建立端口转发,假设WebView调试端口基于进程ID subprocess.run(f'{self.adb_cmd} forward tcp:9222 localabstract:webview_devtools_remote_{pid}', shell=True) # 2. 初始化Selenium,连接到本地9222端口 options = webdriver.ChromeOptions() options.debugger_address = "127.0.0.1:9222" # 需要指定一个chromedriver路径,但它不启动新浏览器,只是作为客户端 self.driver = webdriver.Chrome(options=options) self.wait = WebDriverWait(self.driver, 10) # 3. 现在可以使用Selenium API操作WebView内的元素 try: search_box = self.wait.until(EC.presence_of_element_located((By.ID, "search-input"))) search_box.send_keys("智能手机") search_button = self.driver.find_element(By.CSS_SELECTOR, ".search-btn") search_button.click() # 等待搜索结果加载 self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, "product-item"))) print("WebView内搜索完成") finally: # 注意:不要立即关闭driver,因为后续可能还要操作原生界面 pass4.4 步骤三:返回原生界面加入购物车
假设点击某个商品后,应用会跳转回原生界面展示商品详情和加入购物车按钮。
def test_add_to_cart(self): # 在WebView中点击第一个商品(使用Selenium) first_product = self.driver.find_elements(By.CLASS_NAME, "product-item")[0] first_product.click() time.sleep(3) # 等待原生详情页加载 # 此时上下文已切换回原生应用,关闭Selenium driver self.driver.quit() # 再次使用图像识别定位原生界面上的“加入购物车”按钮 screen = self.take_screenshot() add_cart_pos = self.find_by_template(screen, 'template_add_cart.png') if add_cart_pos: self.adb_shell(f'input tap {add_cart_pos[0]} {add_cart_pos[1]}') time.sleep(1) # 验证:识别屏幕上是否出现“添加成功”的Toast或提示 # 可以通过连续截图,对比识别特定提示文本来实现简单断言 if self.find_text_on_screen("添加成功"): print("加入购物车测试通过") else: print("加入购物车失败")5. 稳定性提升与高级技巧
在实际项目中,直接使用上述基础脚本会非常脆弱。下面分享一些提升脚本稳定性和效率的实战经验。
5.1 等待与同步策略
- 固定等待(Sleep):
time.sleep()是最简单但最不推荐的方式,它浪费大量时间。仅作为最后的手段或在已知的、固定的加载场景下使用。 - 轮询等待(Polling):对于图像识别,实现一个
wait_until_element_appear(template_path, timeout=30)函数,在超时时间内不断截图、识别,直到找到元素或超时。def wait_for_element(self, template_path, timeout=30, interval=1): start = time.time() while time.time() - start < timeout: screen = self.take_screenshot() pos = self.find_by_template(screen, template_path) if pos: return pos time.sleep(interval) raise TimeoutError(f"未在{timeout}秒内找到元素: {template_path}") - Selenium显式等待:在WebView部分,务必使用
WebDriverWait配合expected_conditions,这是Selenium的最佳实践。
5.2 图像识别的鲁棒性优化
- 多模板与置信度:为同一个元素准备多个状态下的模板(如正常按钮、按下状态)。匹配时设置一个合理的置信度阈值(如0.8),并取最高分。
- 缩放与分辨率适配:不同设备分辨率不同。解决方案有两种:一是将所有模板图片和坐标基于一个基准分辨率(如1080x1920)制作,在实际识别前,将截图缩放到基准分辨率;二是使用特征匹配等对尺度变化不敏感的方法。
- 区域限定(ROI):不要在全屏范围内搜索元素。根据应用UI布局,大致确定元素可能出现的区域,只在这个区域内进行识别,可以大幅提升速度和准确性。
- 颜色空间转换:有时元素在灰度图上对比度更高。尝试将截图和模板都转换为HSV或Lab颜色空间,再进行匹配,可能对光照变化有更好的抵抗性。
5.3 异常处理与日志记录
一个健壮的自动化脚本必须能妥善处理异常,并留下清晰的日志供排查。
import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def safe_tap(self, template_path, element_name): try: pos = self.wait_for_element(template_path) self.adb_shell(f'input tap {pos[0]} {pos[1]}') logging.info(f"成功点击: {element_name}") return True except TimeoutError: logging.error(f"等待元素超时: {element_name}") self.take_screenshot(f"error_{element_name}_{int(time.time())}.png") # 保存错误现场截图 return False except Exception as e: logging.error(f"点击元素时发生未知错误: {element_name}, {e}") return False5.4 测试数据与配置分离
将设备序列号、应用包名、Activity名、模板图片路径、等待超时时间等配置信息提取到单独的配置文件(如config.yaml或config.ini)中。这样同一套脚本可以轻松适配不同的测试环境和应用。
6. 常见问题排查与实战避坑指南
即使准备充分,在实际运行中还是会遇到各种问题。这里记录了一些高频问题的解决方案。
问题1:ADB设备离线或未授权
- 现象:
adb devices列表显示设备为offline或unauthorized。 - 排查:
- 重新插拔USB线,确保手机弹出授权对话框并点击“允许”。
- 检查电脑是否安装了正确的手机USB驱动。
- 执行
adb kill-server && adb start-server重启ADB服务。 - 对于无线连接,检查IP地址是否正确,防火墙是否阻止了5555端口。
问题2:图像匹配始终失败
- 现象:脚本找不到模板图片,置信度很低。
- 排查:
- 模板问题:确保模板图片是从当前被测应用相同版本、相同分辨率的设备上截取的,且背景干净。可以使用图像编辑工具将模板裁剪得更精确。
- 截图问题:确认ADB截图是否成功,图片是否损坏。尝试用
PIL或OpenCV打开截图看看。 - 方法问题:尝试更换匹配方法。
cv2.TM_CCOEFF_NORMED通常效果较好。调整阈值(threshold),有时降低到0.7也能接受。 - UI变化:应用UI更新了,模板已过期。需要更新模板库。
问题3:Selenium无法连接到WebView
- 现象:
chrome://inspect页面看不到WebView,或者Selenium连接超时。 - 排查:
- 确认应用内的WebView已启用调试(
setWebContentsDebuggingEnabled(true))。对于非自研应用,此路可能不通。 - 确认使用的ADB端口转发命令正确,且进程ID无误。可以尝试命令
adb forward --list查看所有转发。 - 尝试使用
chrome://inspect页面手动连接,如果能连上,说明环境是通的,问题可能在Selenium配置。
- 确认应用内的WebView已启用调试(
问题4:脚本在CI/CD环境中不稳定
- 现象:本地运行良好,但在Jenkins或GitLab Runner上失败率高。
- 排查:
- 无头环境:CI服务器通常没有图形界面。确保你的图像识别库(如OpenCV)在无头环境下能正常工作(可能需要安装一些额外的系统包,如
libgl1-mesa-glx)。 - 设备状态:CI上设备可能被多个任务共享。脚本开始前,应强制关闭被测应用,清理数据,确保从一个干净的状态开始。
adb shell am force-stop com.example.shop - 并发冲突:如果多任务同时操作一台设备,输入事件会混乱。必须做好设备锁或任务调度。
- 无头环境:CI服务器通常没有图形界面。确保你的图像识别库(如OpenCV)在无头环境下能正常工作(可能需要安装一些额外的系统包,如
问题5:ADB输入文本不支持中文
- 解决方案:这是一个经典限制。变通方案有:
- 如果应用支持,在输入前先点击输入框,然后通过ADB调用系统输入法(如搜狗)的广播来输入,但这很复杂。
- 更实用的方法:在测试设计中规避。例如,登录使用固定的英文测试账号;搜索使用拼音或英文关键词。或者,将需要输入中文的用例标记为手动用例。
- 高级方案:通过
adb shell ime命令切换到一个支持ADB输入的输入法(但需要root权限或修改系统设置,不通用)。
构建这样一套融合方案确实比使用现成的Appium或Airtest门槛更高,但它带来的控制力和灵活性也是无与伦比的。它让你能深入到测试的每一个像素和每一次交互,尤其适合在复杂、定制化的测试场景中追求极致的稳定性和覆盖率。开始可能会觉得繁琐,但一旦这套流程跑通并封装成自己的框架,你会发现很多之前难以自动化的任务,现在都迎刃而解了。
