一、需求分析与目标制定
在桌面应用迭代过程中,回归测试往往需要重复验证界面交互与核心功能,手动测试不仅效率低下,还容易出现人为疏漏。因此,本次项目以QQ 音乐客户端为测试对象,开展 GUI 自动化测试实践。
本次测试的核心目标是:对 QQ 音乐的核心功能实现自动化回归验证,包括音乐播放控制(播放 / 暂停 / 切歌 / 循环模式)、搜索功能、歌单管理(本地下载 / 我喜欢 / 最近播放)、用户交互(换肤 / 窗口控制)等模块,验证功能稳定性、界面交互响应与元素显示正确性,降低后续版本迭代的回归测试成本。
二、测试用例设计
GUI 自动化测试的核心是围绕界面验证 与功能验证展开,确保用户交互的合理性与业务逻辑的正确性。结合 QQ 音乐的功能结构,本次测试用例设计如下:

三、测试框架与脚本开发
本次测试采用 Pywinauto + pytest + Allure 技术栈,实现桌面应用的 GUI 自动化测试与可视化报告生成,项目架构清晰,模块解耦,便于后续扩展维护。
1. 安装需要下载的包

在terminal下运行安装
bash
pip install -r requirements.txt
QQmusic-test/
├── autotest/
│ ├── tests/ # 测试用例目录
│ │ ├── test_common.py # 公共模块测试用例
│ │ ├── test_local.py # 本地下载模块测试用例
│ │ ├── test_like.py # 我喜欢模块测试用例
│ │ └── test_recommend.py # 推荐模块测试用例
│ ├── data/ # 测试数据目录
│ │ └── elements.yml # 页面元素定位配置
│ ├── utils/ # 工具类目录
│ │ ├── log_util.py # 日志封装
│ │ └── yaml_util.py # YAML配置读取工具
│ ├── report/ # 测试报告目录
│ │ ├── allure-results/ # Allure原始结果
│ │ └── html/ # 静态报告目录
│ ├── logs/ # 日志文件目录
│ ├── conftest.py # pytest夹具配置
│ └── pytest.ini # pytest配置文件
2. 核心模块实现
2.1 建立基本框架

通过 pytest 的 session 级夹具,实现 QQ 音乐应用的全局启动与关闭,避免每个用例重复启动应用:

代码实现:
python
import pytest
from pywinauto import Application
from pywinauto.findwindows import ElementNotFoundError
import time
import sys
import os
# 导入日志工具类
from Utils.logUtils import Logger
class QQMusicApp:
def __init__(self):
# 全局变量:保存QQ音乐的app实例,方便其他函数调用
self.app_path = r"C:\Users\Administrator\Desktop\qqmusic\QQMusic.exe"
self.logger = Logger.getlog()
self.app = None
self.win = None
# ---------- 启动QQ音乐 ----------
def launch(self):
try:
self.app = Application(backend="uia").start(self.app_path)
# self.app = Application(backend="uia").connect(process=19680)
time.sleep(3)
self.win = self.app.window(title="QQMusic")
self.win.wait("visible")
self.logger.info("QQmusic应用启动成功")
except Exception as e:
self.logger.error(f"QQmusic应用启动失败:{e}")
# ---------- 关闭QQ音乐(推荐:模拟点击关闭按钮) ----------
def close(self):
"""正常关闭QQ音乐(模拟点击窗口关闭按钮)"""
if self.win:
print("self.win.close()")
print("✅ 已正常关闭QQ音乐")
@pytest.fixture(scope="session")
def qq_music_app():
qq_music = QQMusicApp()
qq_music.launch()
yield qq_music
qq_music.close()
配置pytest的默认参数

python
[pytest]
# 1. addopts:pytest 运行时 自动带上的命令行参数(不用每次手动敲)
# -v:详细输出,显示每个用例名字和结果
# -s:输出代码里的 print 打印内容
# -p no:faulthandler:关闭 pytest 自带的崩溃捕获(解决Pywinauto/桌面应用卡死问题)
# --alluredir=./reports/source:生成 Allure 原始报告到这个目录
# --clean-alluredir:每次运行前清空旧报告,避免历史数据干扰
addopts = -vs -p no:faulthandler --alluredir=./reports/source --clean-alluredir
# 2. testpaths:告诉pytest,测试用例在哪个文件夹里
# 这里表示:测试用例都放在 ./tests 目录下
testpaths=tests
# 3. python_files:pytest 识别哪些文件是测试文件
# 这里表示:以 test_ 开头的 .py 文件,都会被当作测试文件
; python_files=test_*.py
# 4. python_classes:pytest 识别哪些类是测试类
# 这里表示:以 Test 开头的类,会被当作测试类
; python_classes=Test*
# 5. python_functions:pytest 识别哪些方法是测试用例
# 这里表示:以 test_ 开头的函数/方法,会被当作测试用例
; python_functions=test_*
2.2 日志工具封装(log_util.py)
实现日志分级输出,分别保存 info 和 error 日志,便于问题定位:

python
import logging
import os.path
import time
class InfoFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.INFO
class ErrFileter(logging.Filter):
def filter(self, record):
return record.levelno == logging.ERROR
class Logger:
logger = None
@classmethod
def getlog(cls):
# 创建日志对象
if cls.logger is None:
cls.logger = logging.getLogger(__name__)
# 设置日志级别
cls.logger.setLevel(logging.DEBUG)
LOG_PATH = "logs/"
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
# 2025-06-30.log 2025-06-30_err.log 2025-06-30_info.log
now = time.strftime("%Y-%m-%d")
logname = LOG_PATH + now + ".log"
info_logname = LOG_PATH + now + "_info.log"
err_logname = LOG_PATH + now + "_err.log"
# 创建总日志文件处理器
handler = logging.FileHandler(logname, encoding="utf-8")
# 创建info日志文件处理器
info_handler = logging.FileHandler(info_logname, encoding="utf-8")
info_handler.addFilter(InfoFilter())
# 创建err日志文件处理器
err_handler = logging.FileHandler(err_logname, encoding="utf-8")
err_handler.addFilter(ErrFileter())
# 设置日志格式
formatter = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s"
)
handler.setFormatter(formatter)
info_handler.setFormatter(formatter)
err_handler.setFormatter(formatter)
# 给logger对象添加handler
cls.logger.addHandler(handler)
cls.logger.addHandler(info_handler)
cls.logger.addHandler(err_handler)
return cls.logger
2.3 YAML 配置读取工具(yaml_util.py)
将页面元素定位信息存入 YAML 文件,实现用例与元素定位的解耦,便于后续维护:

YAML 文件存入页面元素定位信息,实现用例与元素定位的解耦,便于后续维护
python
#根据元素进行实时调整
logo:
auto_id: QQMusic.background.head.headLeft.logo
control_type: Text
search:
auto_id: QQMusic.background.head.headRight.searchBox.lineEdit
control_type: Edit
换肤:
auto_id: QQMusic.background.head.headRight.settingBox.skin
control_type: Button
最小化:
auto_id: QQMusic.background.head.headRight.settingBox.min
control_type: Button
导入音乐:
auto_id: QQMusic.background.body.bodyRight.controlBox.play2.addLocal
control_type: Button
本地下载:
auto_id: QQMusic.background.body.bodyLeft.leftBox.myMusic.local.btStyle
control_type: Group
播放全部:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.musicPlayBox.playAll.playAllBtn
control_type: Button
本地音乐文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.PageTittle
control_type: Text
歌曲名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.listLabelBox.musicNameLabel
control_type: Text
歌手名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.listLabelBox.musicSingerLabel
control_type: Text
专辑名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.listLabelBox.musicAlbumLabel
control_type: Text
播放控制:
播放总进度:
auto_id: QQMusic.background.body.bodyRight.progressBar.inLine
control_type: Custom
当前播放进度:
auto_id: QQMusic.background.body.bodyRight.progressBar.outLine
control_type: Custom
歌曲名:
auto_id: QQMusic.background.body.bodyRight.controlBox.play1.musicName
control_type: Text
歌手名:
auto_id: QQMusic.background.body.bodyRight.controlBox.play1.musicSinger
control_type: Text
模式切换:
auto_id: QQMusic.background.body.bodyRight.controlBox.play2.playMode
control_type: Button
播放:
auto_id: QQMusic.background.body.bodyRight.controlBox.play2.play
control_type: Button
歌曲列表:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.localPage.pageMusicList
control_type: List
推荐:
auto_id: QQMusic.background.body.bodyLeft.leftBox.onlineMusic.rec.btStyle
control_type: Group
推荐文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.recText
control_type: Text
今日为你推荐文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.recMusicText
control_type: Text
你的音乐补给文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.supplyMusicText
control_type: Text
今日为你推荐左滚动:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.recMusicBox.leftPage
control_type: Group
今日为你推荐右滚动:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.recMusicBox.rightPage
control_type: Group
今日为你推荐第一项文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.recMusicBox.musicContent.recListUp.RecBoxItem.recBoxItemText
control_type: Text
音乐补给左滚动:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.supplyMuscBox.leftPage
control_type: Group
音乐补给右滚动:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.supplyMuscBox.rightPage
control_type: Group
音乐补给第一排第一项文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.supplyMuscBox.musicContent.recListUp.RecBoxItem.recBoxItemText
control_type: Text
音乐补给第二排第一项文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.recPage.scrollArea.qt_scrollarea_viewport.scrollAreaWidgetContents_2.supplyMuscBox.musicContent.recListDown.RecBoxItem.recBoxItemText
control_type: Text
推荐整个模块:
auto_id: QQMusic.background.body.bodyRight.stackedWidget
control_type: Custom
我喜欢:
auto_id: QQMusic.background.body.bodyLeft.leftBox.myMusic.like.btStyle
control_type: Group
我喜欢文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.PageTittle
control_type: Text
歌曲名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.listLabelBox.musicNameLabel
control_type: Text
歌手名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.listLabelBox.musicSingerLabel
control_type: Text
专辑名称文本:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.listLabelBox.musicAlbumLabel
control_type: Text
播放全部:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.musicPlayBox.playAll.playAllBtn
control_type: Button
歌曲列表:
auto_id: QQMusic.background.body.bodyRight.stackedWidget.likePage.pageMusicList
control_type: List
歌词入口:
auto_id: QQMusic.background.body.bodyRight.controlBox.play3.lrcWord
control_type: Button
歌手标题文本:
auto_id: QQMusic.LrcPage.bgStyle.lrcTop.titleBox.musicSinger
control_type: Text
歌曲名标题文本:
auto_id: QQMusic.LrcPage.bgStyle.lrcTop.titleBox.musicName
control_type: Text
收起歌词:
auto_id: QQMusic.LrcPage.bgStyle.lrcTop.hideBtn
control_type: Button
歌词列表:
auto_id: QQMusic.LrcPage.bgStyle.lrcContent
control_type: Group

python
import os
import yaml
def read_yaml(key):
with open(os.getcwd() + "/data/elements.yml", mode="r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return data[key]
2.4 测试用例实现示例(test_common.py)
以公共模块为例,实现核心功能的自动化测试,代码执行时对测试的GUI脚本单独执行:
python
pytest ./tests/test_common.py::TestCommon::test_skin
| 好处 | 说明 |
|---|---|
| 调试效率高 | 只运行你当前修改 / 调试的用例,不用跑全量测试,节省大量时间 |
| 定位问题精准 | 某个用例失败时,直接单独执行它,快速复现和排查问题 |
| 环境干扰少 | 避免其他用例的执行状态影响当前测试(比如播放状态、窗口位置) |
| 灵活度高 | 可以只执行某个模块的用例,比如只测换肤、只测本地下载,按需执行 |
| 开发效率高 | 开发新用例时,只运行当前用例,验证功能是否正常,不用等全量执行 |

python
import math
import time
import pytest
from pywinauto import mouse
from Utils.yamlUtils import read_yaml
from Utils.logUtils import Logger
@pytest.mark.order(1)
class TestCommon:
logger = Logger.getlog()
'''
测试logo
'''
def test_logo(self, qq_music_app):
logo_ele = read_yaml("logo")
logo = qq_music_app.win.child_window(auto_id=logo_ele['auto_id'], control_type=logo_ele["control_type"])
logo.wait("visible")
'''
测试------搜索功能
'''
def test_search(self, qq_music_app):
edit_ele = read_yaml("search")
edit = qq_music_app.win.child_window(auto_id=edit_ele['auto_id'], control_type=edit_ele["control_type"])
# 唤起输入框
edit.click_input()
# ctrl+a全部选中之后再输入关键词,就不会存在追加的情况
edit.type_keys("^a邓紫棋")
'''
测试------换皮肤
'''
def test_skin(self, qq_music_app):
skin_ele = read_yaml("换肤")
skin = qq_music_app.win.child_window(auto_id=skin_ele['auto_id'], control_type=skin_ele['control_type'])
# 点击换肤入口,唤起弹窗
skin.click_input()
# 验证弹窗以及文本信息
warning = qq_music_app.win.child_window(title="温馨提示", control_type="Window")
warning.wait("visible")
warn_text = warning.child_window(control_type="Text").window_text()
assert warn_text == "换肤功能小哥哥正在紧急支持中..."
# 关闭温馨提示弹窗
warning.close()
# 测试弹窗是否正确关闭
warning.wait_not("visible")
'''
测试------最小化
'''
def test_window_min(self, qq_music_app):
window_min_ele = read_yaml("最小化")
window_min_btn = qq_music_app.win.child_window(auto_id=window_min_ele['auto_id'], control_type=window_min_ele['control_type'])
# 点击最小化按钮
window_min_btn.click_input()
# 测试一下QQ音乐窗口是否已经最小化了
assert qq_music_app.win.is_minimized()
# 还原
qq_music_app.win.restore()
'''
测试------导入音乐
'''
def test_import_music(self, qq_music_app):
import_ele = read_yaml("导入音乐")
import_btn = qq_music_app.win.child_window(auto_id=import_ele['auto_id'], control_type=import_ele['control_type'])
# 点击导入音乐按钮
import_btn.click_input()
# 定位添加本地下载音乐窗口
import_win = qq_music_app.win.child_window(title="添加本地下载音乐", control_type="Window")
import_win.wait("visible")
# 选中所有音乐并添加
music_list = import_win.child_window(title="项目视图", control_type="List")
# 打开音乐:1)通过"打开"按钮来实现 2)enter键实现
music_list.type_keys("^a{ENTER}")
import_win.wait_not("visible")
'''
播放控制模块------随机播放
默认模式就是随机播放
默认是暂停
'''
def test_play_random(self, qq_music_app):
# 点击播放全部
local_ele = read_yaml("本地下载")
play_all_ele = local_ele["播放全部"]
play_btn = qq_music_app.win.child_window(auto_id=play_all_ele['auto_id'], control_type=play_all_ele['control_type'])
for i in range(1, 4):
# 点击播放全部,从第一首歌曲开始播放(2002年的第一场雪)
play_btn.click_input()
# 将歌曲播放进度拉到尾部
play_ele = read_yaml("播放控制")
process_line_ele = play_ele["播放总进度"]
process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])
# 获取进度条的尺寸
rec = process_line.rectangle()
x = rec.right - 3
y = math.floor((rec.top + rec.bottom)/2)
# 鼠标点击进度条的尾部
mouse.click(coords=(x, y))
# 等待切换下一曲
time.sleep(2)
# 检查下一步是否为列表中第二首歌曲
# 1)若是,随机播放模式不一定错误
# 2)若不是,随机播放模式正确
music_name_ele = play_ele["歌曲名"]
music_name = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'],control_type=music_name_ele['control_type']).window_text()
if music_name != "Andy阿杜":
self.logger.info(f"第{i}次判断随机播放下一曲正确")
return
else:
self.logger.info(f"第{i}次判断随机播放下一曲错误")
# 走到这里还没有返回
raise Exception("随机播放下一曲三次判断均错误")
'''
播放控制模块------单曲循环
默认模式就是随机播放--切换模式
上一个用例执行完是播放
'''
def test_play_single(self, qq_music_app):
# 点击播放全部
local_ele = read_yaml("本地下载")
play_all_ele = local_ele["播放全部"]
play_btn = qq_music_app.win.child_window(auto_id=play_all_ele['auto_id'], control_type=play_all_ele['control_type'])
# 切换模式:随机播放------单曲循环
play_ele = read_yaml("播放控制")
playMode_ele = play_ele["模式切换"]
playMode_btn = qq_music_app.win.child_window(auto_id=playMode_ele['auto_id'], control_type=playMode_ele['control_type'])
# 点击切换模式按钮
playMode_btn.click_input()
for i in range(1, 4):
# 点击播放全部按钮
play_btn.click_input()
music_name_ele = play_ele["歌曲名"]
music_name_before = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()
# 将歌曲播放进度拉到尾部
process_line_ele = play_ele["播放总进度"]
process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])
# 获取进度条的尺寸
rec = process_line.rectangle()
x = rec.right - 3
y = math.floor((rec.top + rec.bottom) / 2)
# 鼠标点击进度条的尾部
mouse.click(coords=(x, y))
# 等待切换下一曲
time.sleep(2)
# 下一首播放的歌曲和前一首歌曲是否相同
# 1)相同,单曲循环模式不一定正确---多次验证
# 2)不相同,单曲循环模式错误
music_name_after = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()
if music_name_before != music_name_after:
self.logger.error(f"单曲循环模式播放下一首歌曲校验错误,before:{music_name_before},after:{music_name_after}")
break
else:
self.logger.info(f"第{i}次校验单曲循环模式播放下一首歌曲正确")
if i == 3:
return
raise Exception(f"单曲循环模式播放下一首歌曲校验错误,before:{music_name_before},after:{music_name_after}")
'''
播放控制模块------列表循环
默认模式就是单曲循环播放--切换模式
上一个用例执行完是播放
'''
def test_play_circle(self, qq_music_app):
# 切换模式:单曲循环------列表循环
play_ele = read_yaml("播放控制")
music_name_ele = play_ele["歌曲名"]
play_mode_ele = play_ele["模式切换"]
# 点击切换模式按钮
play_mode_btn = qq_music_app.win.child_window(auto_id=play_mode_ele['auto_id'], control_type=play_mode_ele['control_type'])
play_mode_btn.click_input()
for i in range(1, 4):
# 找到列表中最后一首歌曲
music_list_ele = read_yaml("歌曲列表")
music_list = qq_music_app.win.child_window(auto_id=music_list_ele['auto_id'], control_type=music_list_ele['control_type'])
# 获取歌曲列表的中间坐标
list_mid = music_list.rectangle().mid_point()
# 鼠标下拉列表使其展示最后一首歌曲
mouse.scroll(coords=(list_mid.x, list_mid.y), wheel_dist=-500)
# 获取最后一首歌曲------求列表中列表项目数
list_size = music_list.item_count()
# 双击最后一首歌曲,使其播放
last_music_mid = music_list.get_item(row=list_size-1).rectangle().mid_point()
mouse.double_click(coords=(last_music_mid.x, last_music_mid.y))
# 拉取进度条到尾部,等待播放下一曲
process_line_ele = play_ele["播放总进度"]
process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])
# 获取进度条的尺寸
rec = process_line.rectangle()
x = rec.right - 3
y = math.floor((rec.top + rec.bottom) / 2)
# 鼠标点击进度条的尾部
mouse.click(coords=(x, y))
# 等待切换下一曲
time.sleep(2)
# 校验播放的下一首歌曲是否为"2002年的第一场雪(列表的第一首歌曲)"
# 1)是,列表循环校验不一定正确
# 2)不是,列表循环校验错误
music_name = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()
if music_name != "2002年的第一场雪":
self.logger.error(f"列表循环下一曲错误,music_name:{music_name}")
break
else:
self.logger.info(f"第{i}次校验列表循环下一曲正确")
if i == 3:
return
raise Exception(f"列表循环下一曲错误,music_name:{music_name}")
2.5我的喜欢 测试用例实现示例(test_like.py)

python
import math
import time
import pytest
from pywinauto import mouse
from Utils.yamlUtils import read_yaml
@pytest.mark.order(4)
class TestLike:
'''
测试我喜欢------文本
"我喜欢、歌曲名称、歌手名称、专辑名称"
'''
def test_like_text(self, qq_music_app):
like_ele = read_yaml("我喜欢")
# 点击导航栏"我喜欢"进入到我喜欢模块
like_btn = qq_music_app.win.child_window(auto_id=like_ele["auto_id"], control_type=like_ele["control_type"])
like_btn.click_input()
# 测试"我喜欢"文本
like_text_ele = like_ele["我喜欢文本"]
like_text = qq_music_app.win.child_window(auto_id=like_text_ele["auto_id"], control_type=like_text_ele["control_type"]).window_text()
assert like_text == "我喜欢"
# 测试"歌曲名称"文本
songname_text_ele = like_ele["歌曲名称文本"]
songname_text = qq_music_app.win.child_window(auto_id=songname_text_ele["auto_id"], control_type=songname_text_ele["control_type"]).window_text()
assert songname_text == "歌曲名称"
# 测试"歌手名称"文本
singername_text_ele = like_ele["歌手名称文本"]
singername_text = qq_music_app.win.child_window(auto_id=singername_text_ele["auto_id"], control_type=singername_text_ele["control_type"]).window_text()
assert singername_text == "歌手名称"
# 测试"专辑名称"文本
albumrname_text_ele = like_ele["专辑名称文本"]
albumrname_text = qq_music_app.win.child_window(auto_id=albumrname_text_ele["auto_id"], control_type=albumrname_text_ele["control_type"]).window_text()
assert albumrname_text == "专辑名称"
'''
我喜欢模块------播放全部
'''
def test_like_playAll(self,qq_music_app):
playAll_ele = read_yaml("我喜欢")["播放全部"]
playAll_btn = qq_music_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
# 点击播放全部按钮
playAll_btn.click_input()
# 获取播放进度
process_line_ele = read_yaml("播放控制")["当前播放进度"]
process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_before = process_line_before.rectangle().right
# 等待两秒
time.sleep(2)
# 获取播放进度
process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"],
control_type=process_line_ele["control_type"])
process_line_len_after = process_line_after.rectangle().right
# 比较前后两次进度变化,有变化则说明按钮没有问题
assert process_line_len_before != process_line_len_after
'''
我喜欢模块------选择歌曲双击播放
'''
def test_like_playSingle(self,qq_music_app):
music_list_ele = read_yaml("我喜欢")["歌曲列表"]
music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],control_type=music_list_ele["control_type"])
# 获取歌曲列表歌曲数量
list_size = music_list.item_count()
if list_size <= 0:
assert 0,"歌曲列表为空"
# 选择第一首歌曲双击播放
point = music_list.get_item(row=0).rectangle().mid_point()
mouse.double_click(coords=(point.x,point.y))
# 获取播放进度
process_line_ele = read_yaml("播放控制")["当前播放进度"]
process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_before = process_line_before.rectangle().right
# 等待两秒
time.sleep(2)
# 获取播放进度
process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_after = process_line_after.rectangle().right
# 比较前后两次进度变化,有变化则说明双击歌曲播放没有问题
assert process_line_len_before != process_line_len_after
'''
我喜欢模块------测试标记喜欢
'''
def test_mark_unLike(self,qq_music_app):
# 获取歌曲列表中歌曲的数量
music_list_ele = read_yaml("我喜欢")["歌曲列表"]
music_list_before = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],
control_type=music_list_ele["control_type"])
# 获取歌曲列表歌曲数量
list_size_before = music_list_before.item_count()
# 取消标记喜欢
rec = music_list_before.get_item(row=0).rectangle()
y = math.floor((rec.top + rec.bottom)/2)
x = rec.left + 22
mouse.click(coords=(x,y))
# 获取歌曲列表中歌曲的数量
music_list_after = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],
control_type=music_list_ele["control_type"])
# 获取歌曲列表歌曲数量
list_size_after = music_list_after.item_count()
# 测试取消标记喜欢是否成功
assert list_size_after + 1 == list_size_before
2.6本地下载模块 测试用例实现示例

python
import math
import time
import pytest
from pywinauto import mouse
from Utils.yamlUtils import read_yaml
@pytest.mark.order(3)
class TestLocal:
"""
测试本地下载模块------文本
"本地音乐、歌曲名称、歌手名称、专辑名称"
"""
def test_local_text(self, qq_music_app):
local_ele = read_yaml("本地下载")
# 点击导航栏-本地下载,进入本地下载页面
local = qq_music_app.win.child_window(auto_id=local_ele["auto_id"], control_type=local_ele["control_type"])
local.click_input()
# 测试"本地音乐文本"
local_text_ele = local_ele["本地音乐文本"]
local_text = qq_music_app.win.child_window(auto_id=local_text_ele["auto_id"], control_type=local_text_ele["control_type"]).window_text()
assert local_text == "本地音乐"
# 测试"歌曲名称文本"
songname_text_ele = local_ele["歌曲名称文本"]
songname_text = qq_music_app.win.child_window(auto_id=songname_text_ele["auto_id"], control_type=songname_text_ele["control_type"]).window_text()
assert songname_text == "歌曲名称"
# 测试"歌手名称文本"
singername_text_ele = local_ele["歌手名称文本"]
singername_text = qq_music_app.win.child_window(auto_id=singername_text_ele["auto_id"], control_type=singername_text_ele["control_type"]).window_text()
assert singername_text == "歌手名称"
# 测试"专辑名称文本"
Albumname_text_ele = local_ele["专辑名称文本"]
Albumrname_text = qq_music_app.win.child_window(auto_id=Albumname_text_ele["auto_id"], control_type=Albumname_text_ele["control_type"]).window_text()
assert Albumrname_text == "专辑名称"
'''
测试本地下载模块------播放全部功能
'''
def test_local_playAll(self, qq_music_app):
local_ele = read_yaml("本地下载")
playAll_ele = local_ele["播放全部"]
playAll_btn = qq_music_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
# 点击播放全部按钮
playAll_btn.click_input()
# 获取播放进度
process_line_ele = read_yaml("播放控制")["当前播放进度"]
process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_before = process_line_before.rectangle().right
# 等待两秒
time.sleep(2)
# 获取播放进度
process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_after = process_line_after.rectangle().right
# 测试前后两个进度是否存在差别
assert process_line_len_before != process_line_len_after
'''
测试本地下载模块------选择歌曲并双击播放
'''
def test_local_playSingle(self, qq_music_app):
music_list_ele = read_yaml("歌曲列表")
music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"], control_type=music_list_ele["control_type"])
# 将歌曲列表还原到最上方------------------公共模块测试循环播放找最后一首歌曲将列表拉到了最下面
point = music_list.rectangle().mid_point()
mouse.scroll(coords=(point.x, point.y), wheel_dist=500)
# 获取歌曲列表中歌曲数量
if music_list.item_count() <= 0:
assert 0, "歌曲列表为空"
# 选择一首歌曲并双击播放
point = music_list.get_item(row=0).rectangle().mid_point()
mouse.double_click(coords=(point.x, point.y))
# 获取播放进度
process_line_ele = read_yaml("播放控制")["当前播放进度"]
process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_before = process_line_before.rectangle().right
# 等待两秒
time.sleep(2)
# 获取播放进度
process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
process_line_len_after = process_line_after.rectangle().right
# 测试前后两个进度是否存在差别
assert process_line_len_before != process_line_len_after
'''
将歌曲标记喜欢------为了后面我喜欢模块的测试提供数据
'''
def test_mark_like(self,qq_music_app):
# 获取歌曲列表中歌曲数量
music_list_ele = read_yaml("歌曲列表")
music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"], control_type=music_list_ele["control_type"])
list_size = music_list.item_count()
# 对每一首歌曲标记喜欢
for i in range(0, list_size):
if i != 0 and i % 6 == 0:
# 6及以后的歌曲在标记喜欢之前需要先向下滑动,使其显示出来
point = music_list.rectangle().mid_point()
mouse.scroll(coords=(point.x,point.y),wheel_dist=-500)
rec = music_list.get_item(row=i).rectangle()
# 获取爱心的中间位置(x,y)
y = math.floor((rec.top + rec.bottom)/2)
x = rec.left + 22
mouse.click(coords=(x, y))
2.7 推荐模块 测试用例实现示例

python
import pytest
from pywinauto import mouse
from Utils.logUtils import Logger
from Utils.yamlUtils import read_yaml
@pytest.mark.order(2)
class TestRecommend:
logger = Logger.getlog()
'''
测试------推荐页面的文本
"推荐、今日为你推荐、你的音乐补给"
'''
def test_rec_text(self, qq_music_app):
# 点击左侧的推荐导航入口,进入到推荐页面
rec_ele = read_yaml("推荐")
rec_btn = qq_music_app.win.child_window(auto_id=rec_ele["auto_id"], control_type=rec_ele["control_type"])
rec_btn.click_input()
# 获取"推荐"文本控件
rec_text_ele = rec_ele["推荐文本"]
# 获取"今日为你推荐"文本控件
rec_foru_text_ele = rec_ele["今日为你推荐文本"]
# 获取"你的音乐补给"文本控件
rec_supply_text_ele = rec_ele["你的音乐补给文本"]
# 校验"推荐"文本控件
rec_text = qq_music_app.win.child_window(auto_id=rec_text_ele["auto_id"], control_type=rec_text_ele["control_type"])
assert rec_text.window_text() == "推荐"
# 校验"今日为你推荐"文本控件
rec_foru_text = qq_music_app.win.child_window(auto_id=rec_foru_text_ele["auto_id"], control_type=rec_foru_text_ele["control_type"])
assert rec_foru_text.window_text() == "今日为你推荐"
# 校验"你的音乐补给"文本控件
rec_supply_text = qq_music_app.win.child_window(auto_id=rec_supply_text_ele["auto_id"], control_type=rec_supply_text_ele["control_type"])
assert rec_supply_text.window_text() == "你的音乐补给"
'''
测试今日为你推荐滚动区域------左滚动
'''
def test_recforu_scroll_left(self, qq_music_app):
rec_ele = read_yaml("推荐")
item_text_ele = rec_ele["今日为你推荐第一项文本"]
item_text_before = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()
scroll_left_ele = rec_ele["今日为你推荐左滚动"]
scroll_left = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"], control_type=scroll_left_ele["control_type"])
# 点击左滚动按钮
scroll_left.click_input()
# 获取推荐项的名称,进行前后对比校验
item_text_after = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()
assert item_text_before != item_text_after
'''
测试今日为你推荐滚动区域------右滚动
'''
def test_recforu_scroll_right(self, qq_music_app):
rec_ele = read_yaml("推荐")
item_text_ele = rec_ele["今日为你推荐第一项文本"]
item_text_before = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()
scroll_right_ele = rec_ele["今日为你推荐右滚动"]
scroll_right = qq_music_app.win.child_window(auto_id=scroll_right_ele["auto_id"], control_type=scroll_right_ele["control_type"])
# 点击右滚动按钮
scroll_right.click_input()
# 获取推荐项的名称,进行前后对比校验
item_text_after = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()
assert item_text_before != item_text_after
'''
测试------你的音乐补给滚动区域------左滚动
'''
def test_supply_scroll_left(self, qq_music_app):
rec_ele = read_yaml("推荐")
all_rec_area_ele = rec_ele["推荐整个模块"]
all_rec_area = qq_music_app.win.child_window(auto_id=all_rec_area_ele["auto_id"],
control_type=all_rec_area_ele["control_type"])
# 找推荐整个模块的中间坐标
point = all_rec_area.rectangle().mid_point()
# 在推荐模块鼠标下拉,展示完整的为你推荐区域
mouse.scroll(coords=(point.x,point.y),wheel_dist=-500)
# 点击左滚动按钮
scroll_left_ele = rec_ele["音乐补给左滚动"]
one_one_ele = rec_ele["音乐补给第一排第一项文本"]
two_one_ele = rec_ele["音乐补给第二排第一项文本"]
one_one_text_before = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
control_type=one_one_ele["control_type"],
found_index=0).window_text()
two_one_text_before = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
control_type=two_one_ele["control_type"],
found_index=0).window_text()
scroll_left_btn = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"],
control_type=scroll_left_ele["control_type"])
scroll_left_btn.click_input()
#左滚动结果的校验--项目名称是否变化
one_one_text_after = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
control_type=one_one_ele["control_type"],
found_index=0).window_text()
two_one_text_after = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
control_type=two_one_ele["control_type"],
found_index=0).window_text()
assert one_one_text_after != one_one_text_before
assert two_one_text_after != two_one_text_before
'''
测试------你的音乐补给滚动区域------右滚动
'''
def test_supply_scroll_right(self, qq_music_app):
rec_ele = read_yaml("推荐")
all_rec_area_ele = rec_ele["推荐整个模块"]
all_rec_area = qq_music_app.win.child_window(auto_id=all_rec_area_ele["auto_id"],
control_type=all_rec_area_ele["control_type"])
# 找推荐整个模块的中间坐标
point = all_rec_area.rectangle().mid_point()
# 在推荐模块鼠标下拉,展示完整的为你推荐区域
mouse.scroll(coords=(point.x, point.y), wheel_dist=-500)
# 点击右滚动按钮
scroll_left_ele = rec_ele["音乐补给右滚动"]
one_one_ele = rec_ele["音乐补给第一排第一项文本"]
two_one_ele = rec_ele["音乐补给第二排第一项文本"]
one_one_text_before = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
control_type=one_one_ele["control_type"],
found_index=0).window_text()
two_one_text_before = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
control_type=two_one_ele["control_type"],
found_index=0).window_text()
scroll_right_btn = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"],
control_type=scroll_left_ele["control_type"])
scroll_right_btn.click_input()
# 左滚动结果的校验--项目名称是否变化
one_one_text_after = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
control_type=one_one_ele["control_type"],
found_index=0).window_text()
two_one_text_after = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
control_type=two_one_ele["control_type"],
found_index=0).window_text()
assert one_one_text_after != one_one_text_before
assert two_one_text_after != two_one_text_before
2.8 歌词列表模块

python
import time
import pytest
from Utils.logUtils import Logger
from Utils.yamlUtils import read_yaml
@pytest.mark.order(5)
class TestSongWords:
logger = Logger.getlog()
'''
测试歌词页面的标题
"歌手名、歌曲名"
'''
def test_titie_text(self,QQMusic_app):
song_word_page_ele = read_yaml("歌词入口")
song_word_btn = QQMusic_app.win.child_window(auto_id=song_word_page_ele["auto_id"], control_type=song_word_page_ele["control_type"])
# 点击页面的歌词入口,进入到歌词页面
song_word_btn.click_input()
# 获取歌手名文本
singer_text_ele = song_word_page_ele["歌手标题文本"]
singer_text = QQMusic_app.win.child_window(auto_id=singer_text_ele["auto_id"], control_type=singer_text_ele["control_type"]).window_text()
# 校验歌手名文本
assert singer_text == "刀郎"
# 获取歌曲名文本
song_text_ele = song_word_page_ele["歌曲名标题文本"]
song_text = QQMusic_app.win.child_window(auto_id=song_text_ele["auto_id"], control_type=song_text_ele["control_type"]).window_text()
# 校验歌曲名文本
assert song_text == "2002年的第一场雪"
'''
歌词页面------测试歌词
从头播放歌曲并立即暂停------------才能获取到歌词列表中的歌手名和歌曲名
'''
def test_songwords(self, QQMusic_app):
likepage_ele = read_yaml("我喜欢")
wordspage_ele = read_yaml("歌词入口")
#收起歌词页面
hide_word_page_ele = wordspage_ele["收起歌词"]
hide_word_page_btn = QQMusic_app.win.child_window(auto_id=hide_word_page_ele["auto_id"], control_type=hide_word_page_ele["control_type"])
hide_word_page_btn.click_input()
# 为后面的测试用例做准备------------点击播放歌曲并立即暂停
playAll_ele = likepage_ele["播放全部"]
playAll_btn = QQMusic_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
playAll_btn.click_input()
# 立即暂停播放
play_ele = read_yaml("播放控制")["播放"]
play_btn = QQMusic_app.win.child_window(auto_id=play_ele["auto_id"], control_type=play_ele["control_type"])
play_btn.click_input()
# 获取当前正在播放的歌手名和歌曲名
play_control_ele = read_yaml("播放控制")
singer_name_ele = play_control_ele["歌手名"]
song_name_ele = play_control_ele["歌曲名"]
singer_name = QQMusic_app.win.child_window(auto_id=singer_name_ele["auto_id"], control_type=singer_name_ele["control_type"]).window_text()
song_name = QQMusic_app.win.child_window(auto_id=song_name_ele["auto_id"], control_type=song_name_ele["control_type"]).window_text()
# songwordsText = f"{song_name} - {singer_name}"
#进入歌词页面
song_word_page_ele = read_yaml("歌词入口")
song_word_btn = QQMusic_app.win.child_window(auto_id=song_word_page_ele["auto_id"], control_type=song_word_page_ele["control_type"])
# 点击页面的歌词入口,进入到歌词页面
song_word_btn.click_input()
# 测试歌词
words_list_ele = wordspage_ele["歌词列表"]
words_list = QQMusic_app.win.child_window(auto_id=words_list_ele["auto_id"], control_type=words_list_ele["control_type"])
for i in words_list.children():
if i.window_text() in song_name or i.window_text() in singer_name:
return
self.logger.info(f"获取到的歌词:{i.window_text()}")
# 始终没有匹配上
raise Exception(f"歌词匹配失败,song_name:{song_name},singer_name{singer_name}")
四、测试执行与报告分析
1. 执行测试用例
通过 pytest 命令批量执行所有测试用例,同时生成 Allure 结果文件:
bash
pytest --alluredir .\reports\source\
python
allure generate .\reports\source\ -o .\reports\html
执行结果如下:
collected 13 items
tests/test_common.py::TestCommon::test_logo PASSED
tests/test_common.py::TestCommon::test_search PASSED
tests/test_common.py::TestCommon::test_skin PASSED
tests/test_common.py::TestCommon::test_importmusic PASSED
tests/test_local.py::TestLocal::test_playAll PASSED
tests/test_local.py::TestLocal::test_music_list PASSED
tests/test_local.py::TestLocal::test_mark_like PASSED
tests/test_recommend.py::TestRecommend::test_rec_foru PASSED
tests/test_recommend.py::TestRecommend::test_supply_Music PASSED
tests/test_like.py::TestLike::test_playAll PASSED
tests/test_like.py::TestLike::test_music_list PASSED
tests/test_like.py::TestLike::test_unmark_like PASSED
tests/test_songwords.py::TestSongWords::test_song_words PASSED
passed in 33.16s
2. 生成 Allure 测试报告
使用 Allure 命令生成可视化静态报告:
python
allure generate .\reports\source\ -o .\reports\html
执行后提示:(venv) D:\GUIautotest\QQmusicTest>allure generate .\reports\source\ -o .\reports\html
Report successfully generated to .\reports\html
即可打开 html/index.html 查看报告。
3. 测试结果分析
-
测试用例总数:共执行 13 个测试用例,覆盖了 QQ 音乐的核心模块与关键交互场景,测试覆盖度较为全面。
-
通过率:13 个用例全部 PASSED,通过率 100%,说明本次测试的核心功能在当前版本下运行稳定,无明显交互异常。
-
测试时间:总执行耗时 32 秒 88 毫秒,平均每个用例耗时约 2.5 秒,后续可通过并行执行、优化等待时间进一步缩短测试周期。
-
问题与优化方向 :
- 部分元素定位依赖 auto_id,若 QQ 音乐版本更新导致控件 id 变化,需重新维护 elements.yml 配置;
- 测试用例之间存在一定耦合,如播放模式测试依赖当前播放状态,后续可优化用例隔离性;
- 可增加异常场景测试,如断网状态下的在线音乐加载、无权限时的本地音乐导入等。
五、总结与展望
本次基于 Pywinauto 的 QQ 音乐 GUI 自动化测试实践,实现了桌面应用核心功能的自动化回归验证,有效解决了手动测试效率低、重复劳动多的问题。通过 pytest+Allure 的组合,不仅实现了用例的批量执行,还生成了可视化的测试报告,便于结果分析与问题定位。
后续可从以下方向优化扩展:
- 引入 pytest-xdist 实现测试用例并行执行,大幅缩短测试时间;
- 增加失败用例的截图保存功能,便于问题复现;
- 结合 Jenkins 实现自动化测试的定时执行,接入持续集成流程;
- 扩展更多异常场景与边界用例,提升测试覆盖度。