当前位置: 首页 > news >正文

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就万事大吉,但为了自动化脚本的稳定和可移植性,我强烈建议进行独立配置。

安装与路径配置:

  1. 独立SDK Platform-Tools下载:前往安卓开发者官网,直接下载platform-tools压缩包。这比安装完整的Android Studio更轻量,也更容易管理版本。
  2. 系统环境变量配置:解压后,将platform-tools目录的路径(例如D:\android\platform-tools)添加到系统的PATH环境变量中。这是关键一步,确保在任意命令行窗口都能直接调用adb命令。
  3. 验证安装:打开新的命令行终端(CMD或PowerShell),输入adb version。正确输出类似Android Debug Bridge version 1.0.41的信息即表示成功。

设备连接与授权:

  1. USB连接:使用数据线连接安卓手机和电脑。在手机上弹出的“是否允许USB调试?”对话框中,务必勾选“始终允许”,并点击确定。这是自动化脚本能无人值守运行的前提。
  2. 无线连接(可选但推荐):对于需要长期连接或连接多台设备的情况,无线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下编译要点:

  1. 环境准备:安装Qt Creator(建议5.15或以上版本)和对应的Qt库。同时需要安装CMake和Visual Studio(作为C++编译器)。
  2. 获取源码git clone https://github.com/barry-ran/QtScrcpy.git
  3. 使用CMake配置:在Qt Creator中打开项目,使用CMake进行构建。重点在于配置CMakeLists.txt中的选项,例如可以设置默认的投屏码率(-b 8M)和最大尺寸(-m 1920)。
  4. 解决依赖:编译过程中可能会缺少libusb等库,需要根据错误提示手动下载并放置到正确目录。

对于大多数自动化测试场景,我们主要利用QtScrcpy的两个核心能力:

  • 屏幕图像流获取:通过其提供的--record参数或直接调用其底层接口,将屏幕帧保存为图像或视频流,供后续图像分析使用。
  • 键鼠事件转发:QtScrcpy可以将PC端的鼠标点击和键盘输入事件转发到手机。但在自动化中,我们更倾向于用ADB命令来模拟这些事件,因为ADB命令更易于脚本化且不依赖前台窗口焦点。

2.3 Selenium与浏览器驱动匹配

由于我们的目标是测试混合应用中的WebView部分,因此需要配置Selenium来控制PC上的浏览器,以访问或调试手机WebView的内容。

  1. 安装Selenium库:通过pip安装Python版本的Selenium:pip install selenium
  2. 下载浏览器驱动
    • Chrome:下载与你的Chrome浏览器版本完全匹配的chromedriver。可以在Chrome的“关于”页面查看版本号,然后去ChromeDriver官网下载。
    • 其他浏览器:如Firefox需下载geckodriver
  3. 驱动放置:将下载的驱动可执行文件放在一个目录下,并将该目录添加到系统PATH,或者直接在Selenium代码中指定驱动路径。

版本匹配是重中之重。浏览器自动升级后,驱动也必须同步更新,否则Selenium会报错。一个实用的技巧是在脚本初始化时,加入版本检查逻辑,或者使用webdriver-manager这类第三方库自动管理驱动版本。

3. 核心架构设计:打通三者的协作链路

理解了每个工具的作用后,我们需要设计一个清晰的架构,让它们协同工作。核心思路是:以图像为媒介,以ADB为执行器,以Selenium为Web专家

3.1 基于图像识别的元素定位策略

这是替代Appium等框架中“控件树”定位的核心方法。我们无法直接获取安卓原生控件的resource-idxpath,但可以通过“看”屏幕来定位。

  1. 屏幕截图获取

    • 方法A(ADB命令)adb exec-out screencap -p > screen.png。这是最直接的方法,但速度稍慢。
    • 方法B(QtScrcpy接口):通过解析QtScrcpy的视频流,实时截取帧。这种方法更快,可以实现近乎实时的屏幕监控,但对编程能力要求较高。通常可以结合opencv库直接从视频流中读取帧。
  2. 元素定位技术

    • 模板匹配(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等算法进行特征点匹配。这比模板匹配更健壮,但计算量更大。

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上下文。

  1. 启用WebView调试:在安卓应用中,开发者需要启用WebView的调试功能(通常是在WebView类中调用setWebContentsDebuggingEnabled(true))。对于测试自己的应用,这可以做到;对于第三方应用,则取决于其是否开启。
  2. 发现WebView:通过ADB命令adb shell cat /proc/net/unix或检查chrome://inspect页面(对于Chrome内核的WebView),可以找到WebView的调试端口。
  3. 远程连接:Selenium的webdriver.Remote可以连接到一个远程的WebDriver服务。我们需要在PC上启动一个代理(如chromedriver在特定端口),然后通过ADB端口转发,将手机上的WebView调试端口映射到PC。
    adb forward tcp:9222 localabstract:webview_devtools_remote_<进程ID>
    然后,Selenium就可以通过localhost:9222连接到这个WebView,像操作普通网页一样进行自动化测试。

架构流程图(文字描述)

  1. 脚本启动,通过ADB连接设备,并启动QtScrcpy获取屏幕流。
  2. 进入测试用例:判断当前界面是原生页面还是WebView。
  3. 若是原生/混合应用原生部分:通过图像识别(模板匹配/OCR)定位目标元素,计算坐标,调用ADB命令执行操作。
  4. 若是WebView内容:通过ADB端口转发,建立Selenium WebDriver连接,使用Selenium的API(find_element_by_id, xpath等)定位元素并操作。
  5. 操作后,通过图像识别或Selenium的预期条件,判断操作结果(如新页面出现、Toast提示等),进行断言。
  6. 循环执行,直到用例结束。

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 filename

4.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,因为后续可能还要操作原生界面 pass

4.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 False

5.4 测试数据与配置分离

将设备序列号、应用包名、Activity名、模板图片路径、等待超时时间等配置信息提取到单独的配置文件(如config.yamlconfig.ini)中。这样同一套脚本可以轻松适配不同的测试环境和应用。

6. 常见问题排查与实战避坑指南

即使准备充分,在实际运行中还是会遇到各种问题。这里记录了一些高频问题的解决方案。

问题1:ADB设备离线或未授权

  • 现象adb devices列表显示设备为offlineunauthorized
  • 排查
    1. 重新插拔USB线,确保手机弹出授权对话框并点击“允许”。
    2. 检查电脑是否安装了正确的手机USB驱动。
    3. 执行adb kill-server && adb start-server重启ADB服务。
    4. 对于无线连接,检查IP地址是否正确,防火墙是否阻止了5555端口。

问题2:图像匹配始终失败

  • 现象:脚本找不到模板图片,置信度很低。
  • 排查
    1. 模板问题:确保模板图片是从当前被测应用相同版本、相同分辨率的设备上截取的,且背景干净。可以使用图像编辑工具将模板裁剪得更精确。
    2. 截图问题:确认ADB截图是否成功,图片是否损坏。尝试用PILOpenCV打开截图看看。
    3. 方法问题:尝试更换匹配方法。cv2.TM_CCOEFF_NORMED通常效果较好。调整阈值(threshold),有时降低到0.7也能接受。
    4. UI变化:应用UI更新了,模板已过期。需要更新模板库。

问题3:Selenium无法连接到WebView

  • 现象chrome://inspect页面看不到WebView,或者Selenium连接超时。
  • 排查
    1. 确认应用内的WebView已启用调试(setWebContentsDebuggingEnabled(true))。对于非自研应用,此路可能不通。
    2. 确认使用的ADB端口转发命令正确,且进程ID无误。可以尝试命令adb forward --list查看所有转发。
    3. 尝试使用chrome://inspect页面手动连接,如果能连上,说明环境是通的,问题可能在Selenium配置。

问题4:脚本在CI/CD环境中不稳定

  • 现象:本地运行良好,但在Jenkins或GitLab Runner上失败率高。
  • 排查
    1. 无头环境:CI服务器通常没有图形界面。确保你的图像识别库(如OpenCV)在无头环境下能正常工作(可能需要安装一些额外的系统包,如libgl1-mesa-glx)。
    2. 设备状态:CI上设备可能被多个任务共享。脚本开始前,应强制关闭被测应用,清理数据,确保从一个干净的状态开始。adb shell am force-stop com.example.shop
    3. 并发冲突:如果多任务同时操作一台设备,输入事件会混乱。必须做好设备锁或任务调度。

问题5:ADB输入文本不支持中文

  • 解决方案:这是一个经典限制。变通方案有:
    1. 如果应用支持,在输入前先点击输入框,然后通过ADB调用系统输入法(如搜狗)的广播来输入,但这很复杂。
    2. 更实用的方法:在测试设计中规避。例如,登录使用固定的英文测试账号;搜索使用拼音或英文关键词。或者,将需要输入中文的用例标记为手动用例。
    3. 高级方案:通过adb shell ime命令切换到一个支持ADB输入的输入法(但需要root权限或修改系统设置,不通用)。

构建这样一套融合方案确实比使用现成的Appium或Airtest门槛更高,但它带来的控制力和灵活性也是无与伦比的。它让你能深入到测试的每一个像素和每一次交互,尤其适合在复杂、定制化的测试场景中追求极致的稳定性和覆盖率。开始可能会觉得繁琐,但一旦这套流程跑通并封装成自己的框架,你会发现很多之前难以自动化的任务,现在都迎刃而解了。

http://www.jsqmd.com/news/1111807/

相关文章:

  • Botan库实现格式保留加密:原理、代码与数据库集成实战
  • 基于AES算法的图像加密原理与Matlab实现详解
  • 《唤醒你的AI同事:WorkBuddy从零上手》033:数据分析案例
  • OCR(三)windows 环境基于c++的 paddle ocr 编译【CPU版本】
  • AMD Ryzen终极调试指南:5步掌握SMUDebugTool硬件级控制
  • Selenium自动化测试进程清理:钩子程序解决僵尸进程问题
  • Three.js房屋GLB模型:视角驱动边缘透明+自发光渲染方案
  • Adobe-GenP 3.0:终极指南教你3分钟解锁Adobe全套设计软件
  • 写期刊小论文用什么 AI 辅助工具?避坑虚假引用工具完整清单
  • 开源威胁情报库实战指南:从数据解析到自动化集成
  • DAPO:面向真实业务的去中心化自适应策略优化范式
  • Frida动态逆向分析淘特App签名机制:从Hook定位到脚本实战
  • Home Assistant HTTPS配置:Let‘s Encrypt插件与GoDaddy API限制实战解析
  • FiveM服务器可直接部署的加载页资源包,带动态CSS动画、Orbitron字体族与背景音效
  • OpenSSL AES-CBC加密解密C语言实现详解与实战避坑指南
  • AI驱动接口自动化:智能用例生成、执行与报告实战
  • Selenium WebDriver连接Edge浏览器调试端口失败问题全解析与解决方案
  • 如何5分钟搭建现代化企业级管理平台:基于FastAPI+Vue3的完整解决方案
  • 基于Rust构建高性能文件加密工具:从AES-256-GCM到命令行实现
  • Python实现HMAC-SHA256 API签名验证:从原理到工程实践
  • Noto Emoji字体渲染技术深度解析:CBDT与COLRv1架构对比
  • IIS 10 HTTPS SSL/TLS安全通道创建失败深度排查指南
  • Python+Playwright自动化测试框架搭建:从零到实战
  • 机器学习卡通化:从原理到端侧落地的全流程实践
  • Appium Inspector连接失败?5个Desired Capabilities配置坑与排障指南
  • CS2200-CP与PIC18F86J15构建高精度计时系统
  • BurpSuite插件开发实战:自动化检测未授权访问漏洞
  • 【CANdelaStudio-从入门到深入到实战】98 刷写失败后的自动恢复与回滚机制:让ECU从“砖”变回“金”
  • .NET C#国密算法实现指南:SM2/SM3/SM4集成与实战
  • JMeter中文乱码问题深度解析与系统性解决方案