尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QQ音乐桌面客户端GUI自动化测试:从图像识别到稳定落地

QQ音乐桌面客户端GUI自动化测试:从图像识别到稳定落地 接到一个任务对QQ音乐进行GUI自动化测试。听起来不就是把客户端界面点一遍、截图、断言一下吗真正开始做才发现这类项目最磨人的不是“写脚本”而是“怎么能让脚本在第二天继续稳定跑”。QQ音乐的客户端界面复杂、弹窗多、UI还有大量自绘元素直接套用传统的坐标点击或浏览器自动化思路很快会碰壁。这篇文章想完整分享一次针对QQ音乐桌面客户端的GUI自动化测试落地过程包括方案选型、核心场景设计、弹窗治理、断言策略和一堆只有实跑才会踩到的坑。如果你正准备做Windows客户端的GUI自动化或者刚接触Airtest、PyAutoGUI这类工具但还没想清楚怎么落地这篇内容应该能给你一些能直接抄的参考。1. 项目背景与测试目标分析1.1 为什么要拿QQ音乐做GUI自动化测试所有自动化项目的起点都要先回答一个问题这个测试值不值得自动化。QQ音乐这种体量的桌面客户端非常适合作为GUI自动化的练兵场。它不是一个简单的窗口而是一个集搜索、播放、歌单、歌词、MV、在线广告、消息推送于一体的复杂应用。功能更新频繁每次改版都可能影响核心播放路径而测试同学如果全凭手工回归光是“搜索一首歌→点击播放→切歌→暂停”这条主路径每天轮一遍就会让人崩溃。自动化在这里解决的是回归成本问题。通过把“搜索、播放、切歌、停止”这类高频、稳定、重复的GUI操作脚本化在版本更新后自动跑一遍主流程能快速发现界面崩溃、按钮失效、功能逻辑异常等低层问题。我在这次项目中设定的目标不是“接管所有手工测试”而是先把核心用户路径覆盖住保证每次提测后能拿到一份UI层面的“体检报告”。1.2 测试范围哪些场景值得自动化并不是所有GUI操作都适合自动化。QQ音乐的界面里有的功能是动画密集型比如播放页转盘、歌词滚动图像识别很容易被视觉差异干扰有的功能涉及真实支付或账号安全比如绿钻购买自动化去点不仅风险高还容易制造脏数据。因此我在这轮项目里把测试范围明确划分成了三个梯队。第一梯队是必须自动化的启动应用、搜索歌曲、点击播放、暂停、切歌、调节音量、退出登录。这些是每天打开QQ音乐都会走的基础路径交互稳定预期结果明确。第二梯队建议自动化的歌单创建与删除、单曲循环/列表循环切换、歌词开关、收藏操作。这些功能虽然偶尔有UI变化但流程清晰是回归的高频区。第三梯队暂时不自动化的付费购买、账号注册、第三方登录、大量文本输入、涉及验证码的流程。这类操作要么依赖外部系统要么容易触发安全策略硬自动化反而会拉低整套脚本的稳定性。开始时我建议先只做第一梯队等框架稳定后再逐步往第二梯队扩展。不要一上来就想把整个客户端所有按钮都覆盖到那不仅脚本量巨大排障成本也会让你在项目初期就失去继续做下去的信心。2. 自动化测试方案选型与环境搭建2.1 桌面GUI自动化工具对比与选型思路QQ音乐是Windows桌面客户端但解决方案里也要考虑未来可能扩展到Mac和手机端。我最初评估了四类常见工具PyAutoGUI、SikuliX、WinAppDriver以及最终选定的Airtest。这四类工具的核心差异在定位策略上。PyAutoGUI是纯坐标屏幕图像库依赖当前分辨率下的绝对坐标只要窗口位置一变动脚本就全乱套。SikuliX基于OpenCV做图像识别思路和Airtest类似但社区活跃度低对中文输入支持一般遇到复杂动态界面时稳定性不如预期。WinAppDriver走的是微软的UIA控件树对标准Win32控件识别效果好但QQ音乐这类大量自绘UI的应用很多控件暴露不全经常抓不到目标元素。最终选择Airtest主要原因有两个第一它支持图像识别和控件识别两种模式对自绘UI很友好我能把“搜索框”存成一张模板图去匹配不需要依赖控件ID。第二它内置了wait、touch、assert_exists等直接面向GUI测试的API脚本可读性高后续交接和维护都方便。它还配套AirtestIDE可以边录边调特别适合快速搭出第一版脚本。工具定位策略优点缺点适用场景PyAutoGUI坐标截图轻量Python库易上手坐标一成不变断点恢复困难快速验证、临时脚本SikuliX图像识别跨平台可视化脚本社区停滞中文兼容一般简单图像点击WinAppDriverUIA控件树标准控件识别准确自绘UI识别弱环境依赖重Win32/标准控件应用Airtest图像识别控件双模式IDE友好生态活跃复杂动画场景容易误判游戏、客户端、APP GUI2.2 基于Airtest的基础环境准备环境准备看起来简单但几个细节直接决定后续排障体验。我使用的环境是Windows 10 Python 3.9安装Airtest库pip install airtest同时安装了AirtestIDE用于截图和预览模板匹配效果。桌面连接方式很简单Airtest连接Windows桌面后所有操作都是基于全屏坐标进行的所以执行前必须固定测试机的分辨率和显示缩放。QQ音乐客户端的UI在不同分辨率下会发生重排如果脚本在1080P下截图换到2K屏上那一堆图像模板基本全部失效。我在这轮项目里第一件事就是统一测试机配置关闭操作系统“显示缩放”里的“更改文本、应用等项目大小”锁定分辨率为1920×1080。这一步如果不做后面所有脚本稳定性都是空中楼阁。Airtest脚本入口一般这样写from airtest.core.api import * auto_setup(__file__) # 启动QQ音乐 import os os.system(start QQMusic) sleep(5)这里有一个小细节start_app()方法在Windows桌面环境下并不总是可靠所以我直接调用系统命令启动QQ音乐然后等待5秒让主页加载完成。GUI应用启动速度不稳定等待时间宁可多给一点也不要刚启动就急着识别界面元素。2.3 为什么不推荐纯坐标脚本和Web/移动端工具选择框架时我看过很多网上的方案有些人会用Selenium去测QQ音乐网页版有些人用Appium去测手机端还有一些人直接用PyAutoGUI写一堆“点击(100,200)”。我不能说这些方案完全错误但它们都有明显的定位偏差。Selenium是浏览器自动化标准工具适合测网页版但QQ音乐的Web端功能只是客户端功能的子集很多桌面端独有交互根本覆盖不到。Appium确实可以测移动端QQ音乐但和本次“GUI自动化”的需求是两个完全不同的场景。PyAutoGUI如果只是做冒烟点击没问题可一旦测试用例变多纯坐标脚本会变成维护噩梦——QQ音乐窗口稍一拖动脚本就找不到按钮了。这里我特别想聊聊GUI和CLI的区别。命令行工具我们只要输入输出稳定测试起来非常爽但GUI应用有视觉状态、动画、弹窗、鼠标悬停等一大堆隐式状态自动化时不能只关心“有没有执行成功”还要关心“界面最终呈现的状态是否符合预期”。Airtest这种“按图索骥”的方式本质上是把GUI测试当作“视觉回归”来做它虽然比坐标脚本慢但符合人的观察方式也更接近真实用户的使用习惯。3. 核心测试场景设计与脚本落地3.1 启动应用与主界面识别脚本里的第一个步骤是确认QQ音乐主界面已经出现。不能简单sleep(5)就认为界面一定就绪因为冷启动和热启动的加载时间差别很大。最稳妥的方式是等待一个标志性图片出现。我截取了QQ音乐顶部的“搜索框”和左侧导航栏的“推荐”图标作为主界面标志。Airtest提供了wait()方法可以循环等待图片出现超时后再报错。from airtest.core.api import * auto_setup(__file__) def wait_main_page(): wait(Template(rtpl/search_box.png, threshold0.7), timeout15) print(主界面已就绪) wait_main_page()这个threshold参数很关键。默认匹配阈值可能过高在复杂背景下找不到设得过低又容易误匹配到相似图标。我一般先从0.7左右开始调如果匹配不到再往下探但建议不要低于0.6否则很容易点错地方。识别主界面这步相当于把“应用是否正常启动”这个测试点直接盖上了。3.2 搜索歌曲并完成播放搜索播放是QQ音乐的核心路径。虽然听起来只是“输入关键字→回车→点歌曲→点播放”但在GUI自动化里每一步都有小坑。第一步是点击搜索框。我使用模板图片定位搜索框的位置touch(Template(rtpl/search_box.png, threshold0.7))第二步是输入中文关键字。Airtest的text()方法在Windows桌面端对中文输入的支持并不稳定盲用经常遇到输入不上或输入了一半的尴尬。我改用剪贴板粘贴来实现稳定输入import pyperclip pyperclip.copy(周杰伦) keyevent({CTRL}v)用剪贴板粘贴的好处是绕开了输入法问题而且速度也比逐字输入快很多。输入完成后按回车等待搜索结果列表出现。keyevent({ENTER}) wait(Template(rtpl/search_result.png, threshold0.7), timeout10)第三步是点击第一首歌曲的播放按钮。这里需要特别注意搜索结果页的歌曲列表不仅包含歌曲还包含MV、歌手、专辑等多种卡片。为了避免点错我会截取“歌曲名称行”的特征区域然后在这个区域上做小范围点击而不是直接点全屏某个位置。song_row Template(rtpl/song_row.png, threshold0.7) touch(song_row) sleep(1) play_btn Template(rtpl/play_btn.png, threshold0.7) touch(play_btn)实际运行时song_row这张示意图如果包含歌曲名文字换一首歌后就匹配不到。更稳妥的做法是只截取歌曲行左侧的“播放按钮图标”那个图标在不同歌曲之间基本不变。我一开始没有意识到这个问题导致脚本在搜索不同歌手时频繁失败后来补齐了一套通用播放按钮模板才解决。3.3 切歌、进度条与播放状态校验播放状态校验是GUI自动化里最体现“细节”的部分。很多人只判断“点了播放按钮”就结束但GUI测试的价值恰恰在于验证“点完之后界面真的变了”。我最常用的是按钮形态切换法。QQ音乐播放时右下角的播放按钮会从带圆圈的三角形变成两条竖线暂停图标。我分别截图保存了play_btn.png和pause_btn.png通过assert_exists(pause_btn)判断是否已经进入播放状态。def assert_playing(): if exists(Template(rtpl/pause_btn.png, threshold0.7), timeout5): print(播放状态正常) else: raise AssertionError(未检测到暂停按钮播放可能失败)除了按钮形态我还会对播放进度条做一个“动态变化检测”。因为哪怕播放按钮图标因为主题皮肤变化而匹配不到进度条滚动、封面切换等视觉变化依然能证明播放器在工作。from PIL import ImageChops def screen_changed(regionNone, interval2): before snapshot(regionregion) sleep(interval) after snapshot(regionregion) bbox ImageChops.difference(before, after).getbbox() return bbox is not None这个检测的缺点是对动画敏感QQ音乐播放页的歌词动画会导致前后截图差异很大所以不适合用于所有场景。我在实践中最常用的是“暂停按钮存在性校验”只有在暂停按钮模板失效时才启用画面变化检测作为旁路。切歌操作类似点击“下一首”按钮后等待一个足够明显的界面变化。我的做法是先截屏记录当前歌曲名区域点击切歌再等待3秒然后重新识别歌曲名区域如果内容发生变化就认为切歌成功。def switch_song(): touch(Template(rtpl/next_btn.png, threshold0.7)) sleep(3) if screen_changed(region[100, 100, 300, 150]): print(切歌成功) else: raise AssertionError(切歌后画面未变化)3.4 非预期弹窗的统一处理策略GUI自动化最大的敌人不是界面复杂而是“非预期弹窗”。我在这个项目里遇到的弹窗五花八门每日推荐弹窗、版本更新提示、VIP活动推广、桌面通知气泡、甚至还有“网络异常重试”对话框。这些弹窗不是每次都会出现但只要出现一次且没被处理后续所有点击都会打在弹窗上造成一连串失败。处理思路是建立“弹窗库”。我把常见弹窗的关闭按钮截图保存下来比如popup_close_1.png、popup_close_2.png然后在每个大步骤前调用一个统一的弹窗巡检函数。POPUP_CLOSE_BTNS [ Template(rtpl/popup_close_1.png, threshold0.7), Template(rtpl/popup_close_2.png, threshold0.7), Template(rtpl/popup_close_3.png, threshold0.7), ] def handle_popups(): for btn in POPUP_CLOSE_BTNS: try: if exists(btn, timeout0.5): touch(btn) print(检测到弹窗并已关闭) except Exception: pass这段代码的关键不是识别弹窗本身而是识别“关闭按钮”。因为关闭按钮在各类弹窗里往往长得差不多识别到它说明当前存在一个弹窗点了它就能恢复主界面。但要注意有些弹窗关闭按钮的位置会变化我看到过右下角、右上角、甚至需要鼠标悬停后才出现的关闭按钮这些就需要把弹窗本身的模板也纳入巡检范围分别处理。更治本的办法是“从源头减少弹窗”。QQ音乐的设置里可以关闭很多运营推广开关比如“每日推荐”“热门推荐”“活动弹窗”。我会在自动化执行机上预先登录一个测试账号把这些开关全部关掉再把消息通知、自动升级也一并禁用。经过这几项设置后弹窗出现的频率会明显下降脚本的稳定性会提升一个档次。3.5 断言、日志与失败现场保留GUI测试有一个很坑的地方失败时如果不保留现场你根本不知道脚本到底卡在哪个画面。所以我从项目一开始就强制要求每个用例的每个关键操作都留下日志和截图。日志不是只记录“点击了搜索框”这种流水账而是要记录当前匹配到的图片路径、匹配阈值、执行耗时以及如果不匹配屏幕上可能存在什么元素。失败时一定要截全屏保存。Airtest自带的snapshot()可以很方便地保存截图。try: touch(Template(rtpl/search_box.png, threshold0.7)) except Exception as e: snapshot(filenamefail_search_box.png) raise e这套“失败留证据”的机制看着简单实际排障时帮了大忙。有一次脚本在搜索步骤偶发失败我拉出几十张失败截图发现失败时间都集中在晚上8点左右那个时间正好是QQ音乐某些运营活动的推送高峰期。如果没有截图我很难把失败和弹窗现象关联起来。除了截图我还把用例执行过程中的关键状态写进结构化日志方便后续分析。具体做法是记录每个步骤开始时的系统时间、识别到的UI状态和匹配耗时形成一份可读的测试报告。3.6 数据驱动维护一批歌曲做回归一个用例如果只搜周杰伦那它只能验证周杰伦搜索结果页是正常渲染的。但要验证QQ音乐整体搜索播放功能就需要覆盖不同类型的歌曲名、不同语言、甚至特殊字符。我把测试数据抽离出来放到一个YAML文件里用脚本循环读取。这样就算以后换一批歌也不用改代码。- name: 搜索周杰伦并播放 keyword: 周杰伦 search_box: tpl/search_box.png result_row: tpl/search_result_row.png play_btn: tpl/play_btn.png expect_pause: tpl/pause_btn.png - name: 搜索英文歌曲并播放 keyword: Yesterday Once More search_box: tpl/search_box.png result_row: tpl/search_result_row.png play_btn: tpl/play_btn.png expect_pause: tpl/pause_btn.png然后在Pytest测试函数中读取这个文件参数化执行import pytest import yaml with open(cases.yaml, r, encodingutf-8) as f: test_cases yaml.safe_load(f) pytest.mark.parametrize(case, test_cases) def test_search_and_play(case): search_and_play( keywordcase[keyword], search_boxcase[search_box], play_btncase[play_btn], expect_pausecase[expect_pause], )数据驱动的好处是当产品上线了新歌或界面布局微调后我只需要更新YAML里的模板图片和关键词不用动核心逻辑。这也是把GUI自动化从“一次性脚本”升级成“可持续回归资产”的关键一步。4. 常见问题与排查技巧实录4.1 图片识别不到还误点怎么办GUI自动化里最常见的失败就是模板匹配不到或者更糟——匹配到一个错误的位置。比如搜索框模板图只有一个小图标结果界面上某个按钮和它相似匹配阈值又设低了脚本就会点错地方后续一系列操作全部偏离。我的处理原则是“宁可识别不到报错也不能乱匹配点错”。具体做法把默认匹配阈值尽量保持在0.75以上对关键操作按钮再单独验证一次。比如点击播放按钮前先用exists()确认按钮存在再执行touch()。另外模板图片的尺寸也影响匹配。模板越小包含的细节越少误匹配概率越高。我倾向于截取按钮周围一圈背景色扩大模板面积增加区分度。比如播放按钮不只是截那个三角形而是连同底部的圆形外框一起截这样匹配到的位置更准确。4.2 不同分辨率下模板失效这个问题我在前面环境准备里提过但实际运行中还是会遇到。测试机如果被人调整过“文本缩放比例”那么即使分辨率不变界面渲染出来的元素大小也会变化导致模板完全失效。我最终在脚本里加了一个环境自检启动用例前先检查当前屏幕分辨率和缩放设置如果不符合预期就直接跳过而不是跑了一半才失败。这样虽然损失了一部分自动化覆盖率但至少每次跑出来的结果都可信。还有一种情况是QQ音乐有“界面换肤”功能默认的红色主题换成了其他颜色原先截取的模板会因颜色差异识别不到。我的做法是避开主题色优先使用结构清晰的图标作为定位点比如搜索框的放大镜图标、播放按钮本身的形状。如果主题皮肤是运营活动强制的那就只能提前截图更新模板。4.3 测试环境被污染导致结果不可信GUI自动化的执行环境非常敏感。一次手动登录了测试账号看了一些歌曲播放列表就会被打乱然后自动化脚本跑起来时界面状态就和预期不一致出现各种奇怪失败。针对这个问题我在执行机上单独建了一个“自动化专用账号”禁用个人动态、关闭个性化推荐不在自动机上做任何人工操作。每天早上跑自动化之前还会有一键清理流程关闭QQ音乐进程、删除缓存目录、重启应用。这样做虽然简单粗暴但让测试数据的干净程度大幅提升。import subprocess def reset_environment(): subprocess.run([taskkill, /f, /im, QQMusic.exe], capture_outputTrue) sleep(2) # 按需清理缓存目录 # shutil.rmtree(os.path.expanduser(~/AppData/Roaming/腾讯/QQMusic/cache), ignore_errorsTrue) os.system(start QQMusic) sleep(8)注意清理缓存目录一定要确认服务端数据不受影响只删除本地临时缓存。千万别把用户配置也清了否则登录态丢失后续又要重新处理登录验证。4.4 执行效率低与并行方案GUI自动化天然比接口测试慢因为每个操作都有真实的界面渲染、图片识别和等待时间。一套核心播放路径跑下来即使全绿也要10分钟左右。如果测试用例数量多串行会跑很久。并行是提效方向但GUI自动化并行后问题更多。多个Airtest实例同时操作同一个Windows桌面会互相抢占鼠标和键盘结果就是画面错乱。我尝试过用多个虚拟机或云手机分别跑不同用例效果还可以但成本高。对于QQ音乐桌面端项目我目前更倾向于“串行跑核心路径 关键用例单独抽出来并行”。从性价比来看与其堆并行不如先把串行脚本的稳定性打磨好。稳定之后再把最关心的几个核心用例放进集成的定时任务里每天凌晨跑一次早上上班看报告。这样既不影响日常工作又不会因为一个弹窗导致所有用例连锁失败。5. 落地GUI自动化的几点心得5.1 从“能跑”到“稳定跑”的转变刚写完第一版搜索播放脚本时我感觉自己已经掌握了GUI自动化。用例跑通的那一瞬间确实很有成就感但第二天重跑失败率直接超过一半。我开始意识到“能跑”和“稳定跑”之间隔着的不是代码水平而是对应用行为的深入理解。之后我花了很多时间在“减弹窗”和“减环境变化”上比如关闭所有通知、统一分辨率、使用专用账号、固定测试机不给人动。做完这些之后脚本的通过率才能稳定在95%以上。如果你正准备开始类似项目我建议不要急着扩展用例数量先把三个核心路径跑到连续三天全绿再逐步加量。5.2 结合接口自动化别让UI扛所有事GUI自动化不是万能的它只适合验证界面交互和视觉状态并不擅长验证数据的准确性和服务端的计算逻辑。比如QQ音乐的VIP歌曲列表是否完整、播放统计是否入账这些都应该交给接口自动化去覆盖。我在这套项目里把任务分成两层GUI层只负责“用户操作路径是否走得通”接口层负责“数据是否正确”。只有在UI和接口都通过时核心功能才算真正测试完成。这个思路帮我省去了大量不必要的GUI定位成本也让测试结果更加可靠。5.3 后续扩展接入CI和统一报告平台脚本稳定之后自然要想办法让它自动化运行。我在实际项目里把它挂到了定时任务里每天凌晨执行跑完把结果汇总成HTML报告并推送通知到工作群。这一步操作不复杂但对团队的价值提升非常明显——大家每天早上都能看到昨天的回归结果有问题第一时间介入。GUI自动化框架的下一步是可以把用例管理、模板管理、报告展示放到一个统一平台上让不懂代码的测试同事也能通过界面维护自己的用例。不过这一步不用急着做先把当前的路走稳等积累到一定量级平台化就是水到渠成的事。最后再分享一个小技巧在每次执行QQ音乐GUI自动化前我会手动打开一次客户端确认登录状态和界面布局都正常再让脚本接管。这个习惯看似多余但实际上帮我排掉了大量由于前一天手动操作留下的“环境残留”。GUI自动化不是把脚本写出来就结束了真正难的是让它成为每天都能依赖的稳定工具这点需要耐心也需要不断和真实的界面行为较劲。
返回列表