基于Scrcpy与ADB的轻量级Android自动化测试方案实践
1. 项目概述与核心价值
最近在折腾一个手机应用的自动化测试项目,传统的Appium方案虽然成熟,但启动慢、环境依赖重,对于需要快速验证或者高频次执行的场景,总感觉有点“杀鸡用牛刀”。后来,我把目光投向了Scrcpy和ADB命令的组合。你可能知道Scrcpy是个开源的手机投屏工具,延迟极低,画质清晰,但很多人可能没意识到,当它和ADB命令结合起来,能形成一个非常轻量、高效的自动化测试新思路。这个方案的核心,就是利用Scrcpy实现实时、低延迟的手机屏幕监控,同时通过Python调用ADB命令,实现精准的屏幕坐标模拟点击、滑动等操作。它特别适合那些对执行速度有要求、或者环境搭建希望尽可能简单的测试场景,比如冒烟测试、核心功能回归、甚至是简单的UI遍历。
简单来说,这个思路就是把“眼睛”(Scrcpy实时画面)和“手”(ADB命令)分开,但又通过Python脚本紧密协同。你不再需要启动一个庞大的Appium Server,也不需要处理复杂的WebDriver协议,直接用最底层的ADB指令去操控设备,响应速度非常快。对于测试开发工程师或者有一定Python基础的测试同学来说,这是一个能极大提升效率、降低复杂度的实用技巧。接下来,我就把这个方案的完整设计、核心实现细节以及我踩过的坑,毫无保留地分享给你。
2. 技术栈选型与设计思路拆解
2.1 为什么是Scrcpy + ADB + Python?
在决定采用这个方案之前,我对比过几种主流的移动端自动化方案。Appium功能全面,生态好,但需要启动Appium Server,依赖Node.js环境,执行速度相对较慢,更适合复杂、跨平台的E2E测试。而像Airtest这类基于图像识别的框架,虽然对新手友好,但图像匹配本身有性能开销,且对屏幕分辨率、颜色变化比较敏感。
Scrcpy+ADB+Python的组合,优势在于“极致的轻量与直接”。
- Scrcpy:它本身只是一个投屏工具,基于高效的H.264视频流传输。它的价值在于为我们提供了一个近乎实时的手机屏幕“视频流”。我们可以通过获取其窗口句柄或者直接解析其传输的帧数据(虽然复杂些),来实时“看到”手机屏幕。对于自动化测试,实时性意味着我们能立刻知道上一步操作的结果,判断界面是否跳转成功,元素是否出现。
- ADB (Android Debug Bridge):这是Android SDK提供的官方调试工具。
adb shell input系列命令是操控手机的“金手指”,可以直接模拟几乎所有的物理交互:点击 (tap)、滑动 (swipe)、文本输入 (text)、物理按键 (keyevent)。它的执行是立刻生效的,几乎没有延迟,因为这是系统级别的指令。 - Python:作为胶水语言,Python在这里扮演“大脑”和“协调者”的角色。它负责启动和管理Scrcpy进程,通过
subprocess模块调用ADB命令,解析Scrcpy的屏幕信息来计算坐标,并组织整个测试逻辑流。Python丰富的库(如opencv-python用于更高级的图像识别,pynput或pyautogui用于在Scrcpy窗口上模拟点击作为备选方案)也让整个方案的扩展性很强。
这个组合的设计思路是“观察-决策-执行”的循环。Python脚本持续从Scrcpy“观察”屏幕状态,根据预设的逻辑或图像识别结果“决策”下一步操作,然后通过ADB“执行”对应的点击或滑动命令。它剥离了重型测试框架的中间层,直连设备核心接口,因此速度飞快,资源占用小。
2.2 核心组件与工作流程
整个方案可以拆解为以下几个核心组件,它们协同工作的流程如下图所示(概念描述):
- 设备连接与ADB环境:确保手机通过USB或网络连接到电脑,且ADB命令可用。这是所有操作的基础。
- Scrcpy投屏服务:在后台启动Scrcpy,将手机屏幕投射到电脑上的一个窗口。我们可以选择让这个窗口可见(用于人工监控)或不可见(纯后台获取画面数据,需要额外处理)。
- 屏幕信息获取模块:这是关键。我们需要获取当前投屏窗口的实时图像,并建立手机屏幕坐标与投屏窗口坐标(或图像像素坐标)之间的映射关系。因为ADB命令操作的坐标是基于手机屏幕物理分辨率的,而我们在电脑上看到的是经过缩放的窗口。
- ADB命令执行模块:Python通过
subprocess调用adb shell input等命令,将计算好的坐标或指令发送给手机。 - 测试逻辑控制器(Python主脚本):这是大脑,它包含测试用例,控制着整个“观察-决策-执行”的循环。例如,它可能先截图,然后寻找“登录”按钮的图案,找到后计算其中心坐标,最后发送
adb shell input tap x y命令。
注意:这里有一个常见的误解。我们并非直接点击Scrcpy的窗口来操控手机(虽然可以,但那属于桌面自动化范畴,不稳定)。我们是通过分析Scrcpy窗口的画面,计算出目标在真实手机屏幕上的坐标,然后让ADB去点击那个坐标。这是两种完全不同的技术路径,后者更稳定、更底层。
3. 环境搭建与核心工具详解
3.1 ADB的安装与配置
ADB是整个方案的基石。如果你的电脑还没有ADB,安装步骤如下:
- 下载Android SDK Platform-Tools:这是最纯净的方式。去Android开发者官网,找到“Command line tools only”进行下载,解压后里面就包含
adb可执行文件。或者直接搜索“Platform-Tools”下载独立包。 - 配置系统环境变量:将解压后存放
adb.exe(Windows)或adb(Mac/Linux)的目录路径,添加到系统的PATH环境变量中。 - 验证安装:打开命令行终端(CMD、PowerShell或Terminal),输入
adb version。如果能看到版本号信息,说明配置成功。
接下来是连接手机:
- 开启手机的“开发者选项”。通常在“关于手机”里连续点击“版本号”7次。
- 在“开发者选项”中,开启“USB调试”。
- 用USB数据线连接手机和电脑。此时手机会弹出“是否允许USB调试”的授权对话框,勾选“始终允许”并确认。
- 在电脑终端输入
adb devices。如果看到设备列表中出现你的设备序列号,且状态为device,则表示连接成功。如果状态是unauthorized,检查手机上的授权提示。
实操心得:推荐使用USB连接,比无线ADB连接更稳定,延迟更低。如果必须用无线,先用USB执行
adb tcpip 5555,再adb connect 手机IP:5555。另外,有些手机品牌(如华为、小米)可能需要额外在开发者选项里打开“USB调试(安全设置)”或安装特定的手机助手驱动才能被ADB识别,这点需要留意。
3.2 Scrcpy的安装与基本使用
Scrcpy的安装非常简单:
- 下载:前往Scrcpy的GitHub发布页面,下载对应你操作系统的安装包(如.exe, .dmg, 或AppImage)。
- 安装/解压:Windows下直接运行安装程序;Mac可能需拖动到应用程序文件夹;Linux解压即可。
- 基本使用:连接手机后,直接双击运行Scrcpy,手机屏幕应该就会投射到电脑窗口上。你可以用鼠标在窗口里点击、拖动来操作手机(这是Scrcpy自带的基本映射功能,但我们自动化不用这种方式)。
Scrcpy有一些非常实用的启动参数,对我们的自动化场景有帮助:
--no-control: 启动Scrcpy但不接收电脑的输入(鼠标键盘),仅投屏。这样能防止手动误操作干扰自动化脚本。--bit-rate 2M: 设置视频码率,降低码率可以节省CPU和带宽,但画质会下降。对于自动化,清晰识别按钮即可,可以设低些。--max-size 1024: 将手机屏幕分辨率限制为1024宽度,等比例缩放。降低分辨率可以进一步提升性能。--window-title ‘自动化测试监控’: 自定义投屏窗口的标题,方便我们用Python脚本找到这个特定窗口。
例如,一个适合自动化后台运行的命令可能是:scrcpy --no-control --bit-rate 2M --max-size 800 --window-title AutoTestMonitor
3.3 Python环境与必要库
你需要一个Python环境(建议3.7以上)。然后通过pip安装以下库:
opencv-python: 用于图像处理和识别(如果需要基于图像查找元素)。pillow: 另一个常用的图像处理库,有时比OpenCV更轻便。pyautogui: 可以用于获取屏幕截图、获取窗口位置,作为获取Scrcpy窗口画面的一种备选方案(并非必须,我们有其他方法)。numpy: OpenCV的依赖,也是处理图像数组的基础。
安装命令:pip install opencv-python pillow pyautogui numpy
4. 核心实现:屏幕坐标映射与ADB操控
这是整个方案的技术核心,理解了这里,就掌握了精髓。
4.1 获取Scrcpy窗口画面与坐标映射原理
我们需要从Scrcpy投屏窗口实时“抓取”画面,并知道画面上的一个点对应手机屏幕上的哪个坐标。
方法一:通过窗口句柄截图(推荐,更稳定)这种方法不依赖Scrcpy的特殊接口,通用性强。我们利用Python的pyautogui或win32gui(Windows)等库,先找到Scrcpy窗口,然后对其进行截图。
import pyautogui import cv2 import numpy as np def find_and_capture_scrcpy(window_title="AutoTestMonitor"): """ 通过窗口标题查找Scrcpy窗口并截图。 注意:此方法要求Scrcpy窗口在屏幕前台且未被最小化。 """ # 获取所有窗口信息,找到目标窗口 # 注意:pyautogui的getWindowsWithTitle在某些环境下可能不准,这里用简化描述。 # 实际生产代码可能需要使用win32gui (Windows) 或 Xlib (Linux) 来精确获取窗口句柄和位置。 # 假设我们已经获得了窗口的左上角坐标(x, y)和宽高(width, height) window_x, window_y, window_width, window_height = 100, 100, 800, 450 # 示例值,实际需动态获取 # 截取整个屏幕 full_screenshot = pyautogui.screenshot() # 转换为OpenCV格式 full_screenshot_cv = cv2.cvtColor(np.array(full_screenshot), cv2.COLOR_RGB2BGR) # 裁剪出目标窗口区域 window_image = full_screenshot_cv[window_y:window_y+window_height, window_x:window_x+window_width] return window_image, (window_x, window_y, window_width, window_height) # 获取映射关系的关键:我们需要知道手机的真实分辨率。 # 通过ADB命令获取:`adb shell wm size` import subprocess def get_phone_resolution(): result = subprocess.run(['adb', 'shell', 'wm', 'size'], capture_output=True, text=True, encoding='utf-8') output = result.stdout.strip() # 输出格式通常为:Physical size: 1080x2340 或 1080x2340 if 'Physical size:' in output: resolution_str = output.split(':')[1].strip() else: resolution_str = output width, height = map(int, resolution_str.split('x')) return width, height phone_width, phone_height = get_phone_resolution() window_img, (win_x, win_y, win_w, win_h) = find_and_capture_scrcpy() # 坐标映射函数:将窗口内的一个像素点坐标,转换为手机屏幕坐标。 def window_point_to_phone_point(win_point_x, win_point_y): """ win_point_x, win_point_y: 在Scrcpy窗口图像内的坐标(以窗口左上角为原点)。 """ # 计算在窗口内的比例位置 ratio_x = win_point_x / win_w ratio_y = win_point_y / win_h # 映射到手机屏幕 phone_x = int(phone_width * ratio_x) phone_y = int(phone_height * ratio_y) return phone_x, phone_y原理:Scrcpy默认会将手机屏幕等比例缩放以适应窗口(或按指定max-size缩放)。只要窗口内容没有被裁剪(黑边是等比例缩放的结果),那么窗口图像上的相对位置比例与手机屏幕上的相对位置比例就是一致的。因此,我们通过adb shell wm size获取手机物理分辨率,然后根据目标点在窗口图像上的坐标比例,就能换算出在手机上的绝对坐标。
重要注意事项:如果Scrcpy启动时加了
--crop参数裁剪了屏幕,或者窗口被手动拉伸变形,这个比例映射关系就会被破坏。因此,为了自动化稳定,建议固定Scrcpy的启动参数(如--max-size),并确保脚本运行时窗口大小和比例不变。
方法二:从Scrcpy直接获取视频帧(高级)Scrcpy本身是通过Socket传输H.264视频流的。理论上,我们可以绕过GUI窗口,直接连接到这个视频流并解码帧。这种方法更高效,不依赖屏幕截图,但实现复杂,需要解析Scrcpy的通信协议和解码视频流。对于大多数自动化场景,方法一已经足够可靠和简单。
4.2 ADB命令模拟用户操作详解
获取到手机屏幕坐标后,就可以用ADB命令进行操控了。adb shell input是核心命令集。
模拟点击 (Tap):
adb shell input tap <x> <y><x>和<y>就是我们上一步计算出的手机屏幕坐标。例如:adb shell input tap 500 1200模拟滑动 (Swipe):
adb shell swipe <x1> <y1> <x2> <y2> [duration(ms)]从点
(x1, y1)滑动到点(x2, y2)。可选的duration参数表示滑动过程的耗时(毫秒),模拟慢速滑动。例如:adb shell swipe 500 1600 500 800 500表示从中间偏下向上滑动,耗时500毫秒。模拟按键 (Keyevent):
adb shell input keyevent <keycode>模拟物理按键,如HOME键、返回键、电源键等。常用键值:
3: HOME4: BACK(返回)24: VOLUME_UP(音量加)25: VOLUME_DOWN(音量减)26: POWER(电源键)66: ENTER(回车)67: DEL(删除) 例如:adb shell input keyevent 4模拟按下返回键。
输入文本 (Text):
adb shell input text "hello world"直接输入文本,相当于在光标处打字。注意:不能输入中文和特殊字符(如空格在早期版本需用
%s代替,现在一般直接支持空格)。
在Python中,我们使用subprocess模块来调用这些命令:
import subprocess def adb_tap(x, y): """执行点击操作""" subprocess.run(['adb', 'shell', 'input', 'tap', str(x), str(y)], check=True) def adb_swipe(x1, y1, x2, y2, duration=None): """执行滑动操作""" cmd = ['adb', 'shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y2)] if duration: cmd.append(str(duration)) subprocess.run(cmd, check=True) def adb_keyevent(keycode): """执行按键操作""" subprocess.run(['adb', 'shell', 'input', 'keyevent', str(keycode)], check=True) def adb_text(text): """输入文本""" # 需要对文本进行简单转义,确保shell命令正确 subprocess.run(['adb', 'shell', 'input', 'text', text], check=True)4.3 结合图像识别定位元素(进阶)
单纯依赖固定坐标的点击非常脆弱,屏幕分辨率一变或者UI稍微改动,脚本就失效了。因此,在实际项目中,我们通常需要图像识别来动态定位元素。
我们可以用OpenCV的模板匹配功能。思路是:事先截取好需要点击的按钮图标(作为模板),在实时获取的Scrcpy窗口画面中搜索这个模板,找到匹配度最高的位置,计算其中心坐标,再映射为手机坐标进行点击。
import cv2 def find_template_on_screen(screen_image, template_path, threshold=0.8): """ 在屏幕图像中查找模板。 :param screen_image: 屏幕截图(OpenCV格式,BGR或灰度)。 :param template_path: 模板图片路径。 :param threshold: 匹配度阈值,高于此值认为匹配成功。 :return: (found, center_x, center_y) found为是否找到,center为在screen_image中的坐标。 """ # 读取模板 template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE) if template is None: raise FileNotFoundError(f"模板图片未找到: {template_path}") # 将屏幕图像转为灰度 gray_screen = cv2.cvtColor(screen_image, cv2.COLOR_BGR2GRAY) # 进行模板匹配 result = cv2.matchTemplate(gray_screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) # TM_CCOEFF_NORMED方法下,最大值越接近1,匹配度越高 if max_val >= threshold: # 计算模板中心点在屏幕图像中的坐标 h, w = template.shape top_left = max_loc center_x = top_left[0] + w // 2 center_y = top_left[1] + h // 2 return True, center_x, center_y else: return False, 0, 0 # 在自动化脚本中使用 window_img, window_info = find_and_capture_scrcpy() found, btn_center_x, btn_center_y = find_template_on_screen(window_img, 'template_login_button.png') if found: # 将窗口坐标转换为手机坐标 phone_x, phone_y = window_point_to_phone_point(btn_center_x, btn_center_y) # 执行点击 adb_tap(phone_x, phone_y) print(f"已点击登录按钮,手机坐标: ({phone_x}, {phone_y})") else: print("未找到登录按钮")实操心得:模板匹配对图像的亮度、旋转、缩放比较敏感。为了提高鲁棒性,可以:
- 使用多个模板(不同状态下的按钮)。
- 对屏幕图像进行预处理,如灰度化、直方图均衡化。
- 考虑使用更高级的特征匹配方法,如SIFT或ORB(OpenCV内置),但它们计算量更大。
- 最实用的方法是:确保测试环境(手机型号、分辨率、系统主题)相对固定,并截取高质量的模板图片。
5. 完整自动化测试脚本架构与实战
现在,我们把所有模块组合起来,构建一个完整的、可复用的自动化测试脚本框架。
5.1 脚本框架设计
一个健壮的脚本框架应该包含以下部分:
- 配置管理:存放设备信息、Scrcpy参数、模板图片路径、坐标配置等。
- 设备控制层:封装ADB命令操作(点击、滑动等)和屏幕截图/获取函数。
- 元素识别层:封装图像识别逻辑,返回找到的元素坐标(手机坐标)。
- 业务流程层:将具体的测试用例编写成一个个函数,调用设备控制和元素识别层。
- 日志与报告:记录操作步骤、成功失败信息,并生成简单报告。
- 异常处理与等待:处理元素未找到、操作超时等异常,并实现智能等待(如等待某个元素出现)。
下面是一个简化的框架示例:
# config.py PHONE_RESOLUTION = (1080, 2340) # 你的手机分辨率 SCRCPY_WINDOW_TITLE = "AutoTestMonitor" TEMPLATES_DIR = "./templates/" LOG_FILE = "./automation.log" # device_controller.py import subprocess import time from config import PHONE_RESOLUTION # ... 导入之前定义的 adb_tap, adb_swipe, get_phone_resolution, find_and_capture_scrcpy, window_point_to_phone_point 等函数 ... class DeviceController: def __init__(self): self.phone_width, self.phone_height = PHONE_RESOLUTION # 可以在这里启动Scrcpy进程 self.scrcpy_process = None self.start_scrcpy() def start_scrcpy(self): """启动Scrcpy投屏""" import subprocess scrcpy_cmd = ['scrcpy', '--no-control', '--bit-rate', '2M', '--max-size', '800', '--window-title', SCRCPY_WINDOW_TITLE] self.scrcpy_process = subprocess.Popen(scrcpy_cmd) time.sleep(3) # 等待Scrcpy启动稳定 def stop_scrcpy(self): """停止Scrcpy投屏""" if self.scrcpy_process: self.scrcpy_process.terminate() self.scrcpy_process.wait() def get_screen_and_map_info(self): """获取当前屏幕和映射信息""" window_img, (win_x, win_y, win_w, win_h) = find_and_capture_scrcpy(SCRCPY_WINDOW_TITLE) return window_img, win_w, win_h def tap_on_phone(self, phone_x, phone_y): adb_tap(phone_x, phone_y) def swipe_on_phone(self, start_x, start_y, end_x, end_y, duration=300): adb_swipe(start_x, start_y, end_x, end_y, duration) # ... 其他设备操作封装 ... # element_finder.py import cv2 from config import TEMPLATES_DIR # ... 导入之前定义的 find_template_on_screen 函数 ... class ElementFinder: def __init__(self, device_controller): self.dc = device_controller def find_element(self, template_name, threshold=0.85, timeout=10, interval=1): """ 在超时时间内循环查找元素。 :param template_name: 模板文件名(不含路径)。 :param threshold: 匹配阈值。 :param timeout: 超时时间(秒)。 :param interval: 每次查找间隔(秒)。 :return: (found, phone_x, phone_y) """ template_path = f"{TEMPLATES_DIR}{template_name}" start_time = time.time() while time.time() - start_time < timeout: window_img, win_w, win_h = self.dc.get_screen_and_map_info() found, center_x, center_y = find_template_on_screen(window_img, template_path, threshold) if found: phone_x, phone_y = window_point_to_phone_point(center_x, center_y, win_w, win_h, self.dc.phone_width, self.dc.phone_height) return True, phone_x, phone_y time.sleep(interval) return False, 0, 0 # test_cases.py import logging from device_controller import DeviceController from element_finder import ElementFinder logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def test_login(): """测试登录流程""" dc = DeviceController() ef = ElementFinder(dc) logging.info("开始登录测试") # 1. 查找并点击“我的”tab found, x, y = ef.find_element("tab_mine.png", timeout=5) if found: dc.tap_on_phone(x, y) logging.info(f"点击‘我的’ tab,坐标({x}, {y})") else: logging.error("未找到‘我的’ tab,测试终止") return False time.sleep(2) # 等待页面跳转 # 2. 查找并点击“登录/注册”按钮 found, x, y = ef.find_element("btn_login_entry.png") if found: dc.tap_on_phone(x, y) logging.info(f"点击登录入口,坐标({x}, {y})") else: logging.error("未找到登录入口") return False time.sleep(2) # 3. 查找用户名输入框并点击,然后输入文本 found, x, y = ef.find_element("input_username.png") if found: dc.tap_on_phone(x, y) # 点击后,输入法通常会弹出,直接使用ADB输入文本(不依赖焦点) dc.adb_text("testuser@example.com") # 假设dc里有这个封装方法 logging.info(f"在用户名输入框输入文本") else: logging.error("未找到用户名输入框") return False # 4. 查找密码输入框并点击、输入 # ... 类似操作 ... # 5. 查找并点击“登录”按钮 found, x, y = ef.find_element("btn_login_submit.png") if found: dc.tap_on_phone(x, y) logging.info(f"点击提交登录,坐标({x}, {y})") else: logging.error("未找到登录提交按钮") return False time.sleep(3) # 等待登录结果 # 6. 验证登录成功(例如,查找用户头像或“退出登录”按钮) found, _, _ = ef.find_element("icon_user_avatar.png", timeout=5) if found: logging.info("登录成功验证通过") return True else: logging.error("登录成功验证失败") return False finally: dc.stop_scrcpy() # main.py if __name__ == "__main__": success = test_login() if success: print("测试用例执行通过!") else: print("测试用例执行失败!")5.2 实战:编写一个简单的自动化脚本
假设我们要自动化一个简单的场景:打开某新闻App,滑动几次,然后点击第一条新闻进入详情页。
准备模板图片:用Scrcpy投屏,手动截图保存以下模板:
icon_news_app.png(桌面上的新闻App图标)news_item_1.png(列表第一条新闻的局部特征图,比如标题开头几个字)btn_back.png(详情页的返回按钮)
编写脚本:
# simple_news_auto.py import time from device_controller import DeviceController from element_finder import ElementFinder dc = DeviceController() ef = ElementFinder(dc) try: # 0. 回到桌面(确保起点一致) dc.adb_keyevent(3) # HOME键 time.sleep(1) # 1. 找到并点击新闻App图标 found, x, y = ef.find_element("icon_news_app.png", timeout=5) if found: dc.tap_on_phone(x, y) print("已打开新闻App") else: print("未找到新闻App图标") exit(1) time.sleep(3) # 等待App启动 # 2. 模拟滑动浏览(滑动3次) screen_center_x = dc.phone_width // 2 screen_center_y = dc.phone_height // 2 for i in range(3): # 从屏幕中部偏下滑动到中部偏上 start_y = int(dc.phone_height * 0.7) end_y = int(dc.phone_height * 0.3) dc.swipe_on_phone(screen_center_x, start_y, screen_center_x, end_y, 500) print(f"第{i+1}次滑动") time.sleep(1.5) # 等待滑动动画和内容加载 # 3. 找到并点击第一条新闻 found, x, y = ef.find_element("news_item_1.png", timeout=5) if found: dc.tap_on_phone(x, y) print("已进入新闻详情页") else: print("未找到第一条新闻") exit(1) time.sleep(2) # 4. 简单浏览后,点击返回 # 可以先做点别的,比如等待几秒模拟阅读 time.sleep(3) found, x, y = ef.find_element("btn_back.png", timeout=5) if found: dc.tap_on_phone(x, y) print("已返回新闻列表") else: # 如果没找到返回按钮,用物理返回键 dc.adb_keyevent(4) print("使用物理返回键返回") print("自动化流程执行完毕!") except Exception as e: print(f"执行过程中发生错误: {e}") finally: dc.stop_scrcpy()
这个脚本展示了基本的流程控制、元素查找和操作组合。你可以在此基础上,增加更多的检查点、逻辑判断和异常处理,构建复杂的测试用例。
6. 常见问题、优化技巧与避坑指南
在实际使用这套方案时,我遇到了不少问题,也总结了一些优化技巧。
6.1 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
adb devices显示设备为unauthorized | 手机未授权电脑的USB调试请求。 | 1. 检查手机屏幕是否有授权弹窗,点击“允许”。 2. 重启ADB服务: adb kill-server && adb start-server。3. 更换USB数据线或USB端口。 |
| Scrcpy启动后黑屏或连接失败 | 1. 手机未开启USB调试。 2. 驱动问题(Windows常见)。 3. 手机系统兼容性问题(如ColorOS等定制系统)。 | 1. 确认USB调试已开启。 2. 安装完整的手机厂商USB驱动。 3. 尝试使用Scrcpy的旧版本,或添加 --force-adb-forward参数。 |
| 图像识别始终找不到模板 | 1. 模板图片与屏幕截图差异太大(亮度、缩放、UI更新)。 2. 匹配阈值 ( threshold) 设置过高。3. Scrcpy窗口大小/比例变化导致坐标映射错误。 | 1. 重新截取模板,确保环境一致。对图像进行预处理(灰度、二值化)。 2. 适当降低阈值,如从0.9调到0.7。 3. 固定Scrcpy启动参数,确保脚本获取的窗口尺寸稳定。 |
| ADB点击命令执行了,但手机没反应 | 1. 坐标计算错误,点在了屏幕外或无效区域。 2. 手机屏幕处于锁屏或休眠状态。 3. 当前应用不接收输入事件(如弹窗遮挡)。 | 1. 打印出计算的手机坐标,用adb shell input tap x y手动验证。2. 发送唤醒命令: adb shell input keyevent 26(电源键)然后adb shell input keyevent 82(菜单键解锁,如果设置了密码则无效)。3. 在操作前先点击一下屏幕中央,确保焦点。 |
| 脚本执行速度慢 | 1. 图像识别(模板匹配)耗时。 2. Scrcpy截图或ADB命令有延迟。 3. 循环查找元素的等待间隔 ( interval) 太短,导致CPU占用高。 | 1. 缩小模板图片尺寸;使用灰度图像匹配;考虑在非关键路径使用固定坐标。 2. 降低Scrcpy画质 ( --bit-rate) 和分辨率 (--max-size)。3. 适当增加查找间隔,如从0.5秒增加到1秒。 |
| 在多台设备上运行不稳定 | 不同设备分辨率不同,导致坐标映射和模板匹配失效。 | 1.核心方案:使用基于比例的坐标和图像识别,而非绝对坐标。 2. 为不同分辨率设备准备不同的模板图片集。 3. 使用更鲁棒的图像特征匹配(如ORB)代替模板匹配。 |
6.2 性能与稳定性优化技巧
- 固定环境:这是保证脚本稳定性的第一要务。使用固定的测试手机(或模拟器)、固定的分辨率、固定的系统主题和字体大小。避免在脚本运行期间手动操作手机或电脑。
- 降低图像识别依赖:不是所有操作都需要图像识别。对于位置固定的元素(如底部Tab栏的按钮),可以事先计算好其相对于屏幕的比例坐标,直接使用比例点击,速度极快。
# 假设“首页”Tab在屏幕底部,横向20%的位置 tab_home_x = int(phone_width * 0.2) tab_home_y = int(phone_height * 0.95) # 靠近底部 dc.tap_on_phone(tab_home_x, tab_home_y) - 智能等待代替固定休眠:大量使用
time.sleep是低效的。应该用“查找元素”函数自带的超时等待机制,只在必要时才等待。对于网络加载,可以结合查找“加载中”图标消失的逻辑。 - 错误重试机制:对于非关键性失败(如一次点击没反应),可以加入重试逻辑。
def robust_tap(element_finder, template_name, retries=3): for i in range(retries): found, x, y = element_finder.find_element(template_name, timeout=2) if found: element_finder.dc.tap_on_phone(x, y) # 点击后,可以再查找一个预期出现的元素来确认点击成功 time.sleep(0.5) if element_finder.find_element("expected_element_after_tap.png", timeout=2)[0]: return True print(f"第{i+1}次点击尝试失败或未验证成功") return False - 日志与截图:在关键步骤和失败时,保存当时的屏幕截图和日志。这对于后期调试和生成测试报告至关重要。可以用OpenCV的
cv2.imwrite保存截图,并打上时间戳和步骤名称。
6.3 与主流框架的对比与适用场景
最后,我们来客观看待这个方案,明确它的最佳适用场景。
- 对比Appium:
- 优势:启动快、资源占用小、执行速度极快、环境依赖简单(只需ADB和Scrcpy)、更底层直接。
- 劣势:生态弱,没有现成的Page Object模型、丰富的客户端库;元素定位主要依赖图像,对UI变化更敏感;跨平台能力弱(主要针对Android)。
- 对比Airtest:
- 优势:更轻量(Airtest其实也封装了不少东西),执行逻辑更透明可控,结合Python自由度极高。
- 劣势:Airtest提供了IDE和一站式图像识别方案,上手更快。本方案需要自己搭建框架。
适用场景:
- Android应用的冒烟测试/核心链路回归:需要快速执行,验证主流程是否通畅。
- 重复性高的简单操作自动化:如批量安装/卸载应用、清理缓存、简单的UI遍历。
- 对执行速度有严格要求的场景。
- 作为现有测试框架的补充:在Appium脚本中,穿插使用ADB命令来完成一些特定操作(如截图、拉取日志文件、修改系统设置)。
- 测试开发人员构建轻量级定制化测试工具。
不适用场景:
- 需要精确控件定位和属性验证的复杂测试:Appium的UIAutomator2/XCUITest驱动在这方面是专业且稳定的。
- 跨平台(iOS/Android)测试:本方案严重依赖ADB,是Android专属。
- 对测试脚本可维护性、团队协作要求极高的中大型项目:Appium的生态和模式更适合。
我个人在实际项目中,经常将这种轻量级方案用于开发自测和预合入验证。开发完成后,跑一个5分钟以内的脚本,快速过一遍核心功能,比手动测试高效得多。而对于正式的CI/CD流水线中的UI自动化,我仍然会使用更稳定、可维护性更强的Appium框架。工具没有好坏,只有是否适合当下的场景。希望这个详细的分享,能为你提供一种新的、高效的自动化测试思路和一套可以直接上手实践的工具方法。