前言
本项目对QQ音乐进行自动化测试,版本为22.16(截至今日最新版)。主要用到 pywinauto(不了解的同学请移步:pywinauto核心速查指南-CSDN博客)和 pytest 框架(下文中有pytest框架的讲解,不会的同学可以找到文章对应处查看:接口自动化测试实战项目-CSDN博客)。本文用到的图像识别的图片会放在末尾,大家点击目录即可跳转。话不多说,咱们直接开始:
测试用例设计
项目较大,我们挑选最重要的功能给大家测试:

在讲代码之前,我主要说一下我们的核心思路:
启动程序 -> 定位到程序所在的窗口 -> 对这个窗口及窗口内的控件进行操作 -> 关闭程序
封装启动关闭程序
启动程序
很好理解,不做过多解释(记得将路径换成自己的):
python
qq_path = r"D:\QQMusicCache\QQMusicLyricNew\QQMusic\QQMusic.exe"
self.app=Application(backend='uia').start(qq_path)
定位窗口
这里比较值得注意的一点是:由于我们QQ音乐的启动程序并不是主程序。所以我们并不能通过 Application() 的返回值用 window() 直接定位。我们采用的方法是:用 Desktop 从全局桌面搜索窗口。
python
# 3. 轮询等待主窗口出现(最多尝试30次,每次间隔1秒)
for i in range(30):
# 列表推导式:从所有桌面窗口里筛选出满足条件的窗口
# 条件1:类名为 "TXGuiFoundation"
# 条件2:窗口当前可见
qq_windows = [win for win in desktop.windows()
if win.class_name() == "TXGuiFoundation" and win.is_visible()]
if qq_windows:
# 如果有符合条件的窗口,取第一个(通常只有一个)
self.win = qq_windows[0]
print(f">>> 找到主窗口,当前标题:{self.win.window_text()}")
return # 成功,退出方法
else:
# 还没找到,等1秒再试
time.sleep(1)
# 如果30秒后仍未找到,抛出异常
raise Exception("错误:30秒内未找到 QQ 音乐主窗口")
那为什么我们用的是类名查找窗口?因为QQ音乐的窗口名字是随着音乐名称改变的,规律无常(因此无法使用 title 和 title_re),我们这里采用更稳定的类名查找。
关闭程序
我们直接对窗口close,这个没有什么难度:
python
def close(self):
"""关闭主窗口"""
if self.win:
self.win.close()
print(">>> 窗口已关闭")
else:
print(">>> 没有可关闭的窗口")
封装
我们预期的封装结果时:开始测试时调用启动,结束后自动关闭窗口。
python
@pytest.fixture(scope="session")
def QQMusic_app():
# 准备工作 (Setup)
QQmusic = QQmusicApp() # 创建 QQMusicApp 类的实例
QQmusic.launch() # 启动 QQ 音乐应用
# 将准备好的对象返回给测试用例使用
yield QQmusic
# 清理工作 (Teardown),在测试会话结束后自动执行
QQmusic.close() # 关闭 QQ 音乐应用
这里简单讲一下pytest.fixture:
它将启动和定位 QQ 音乐主窗口的复杂逻辑封装在
QQMusic_app这个 Fixture 内部。任何需要使用 QQ 音乐的测试用例,无需在自己的代码里重复写启动、轮询窗口的步骤 ,只需在参数列表里写上
QQMusic_app,就能拿到一个已经启动并定位好主窗口的QQMusicApp实例。QQ 音乐的启动过程非常耗时(需等待窗口出现、加载界面),
scope="session"决定了这个 Fixture 在整个测试会话(运行pytest命令开始到结束)中只执行一次。
- 无论测试用例是通过还是失败,当整个测试会话结束时,Pytest 都会自动执行
QQmusic.close(),确保 QQ 音乐被关闭,不会残留在系统托盘中影响后续测试或系统环境。
代码如下:
python
import pytest
import time
from pywinauto import Desktop
from pywinauto import Application
class QQMusicApp:
def __init__(self):
self.win = None # 用于保存找到的主窗口对象
self.app = None
def launch(self):
"""启动 QQ 音乐并定位主窗口"""
qq_path = r"D:\QQMusicCache\QQMusicLyricNew\QQMusic\QQMusic.exe"
self.app=Application(backend='uia').start(qq_path)
# 2. 创建桌面对象,用于搜索所有顶层窗口
desktop = Desktop(backend="uia")
# 3. 轮询等待主窗口出现(最多尝试30次,每次间隔1秒)
for i in range(30):
# 列表推导式:从所有桌面窗口里筛选出满足条件的窗口
# 条件1:类名为 "TXGuiFoundation"
# 条件2:窗口当前可见
qq_windows = [win for win in desktop.windows()
if win.class_name() == "TXGuiFoundation" and win.is_visible()]
if qq_windows:
# 如果有符合条件的窗口,取第一个(通常只有一个)
self.win = qq_windows[0]
print(f">>> 找到主窗口,当前标题:{self.win.window_text()}")
return # 成功,退出方法
else:
# 还没找到,等1秒再试
time.sleep(1)
# 如果30秒后仍未找到,抛出异常
raise Exception("错误:30秒内未找到 QQ 音乐主窗口")
def close(self):
"""关闭主窗口"""
if self.win:
self.win.close()
print(">>> 窗口已关闭")
else:
print(">>> 没有可关闭的窗口")
@pytest.fixture(scope="session")
def QQMusic_app():
# 准备工作 (Setup)
QQmusic = QQMusicApp() # 创建 QQMusicApp 类的实例
QQmusic.launch() # 启动 QQ 音乐应用
# 将准备好的对象返回给测试用例使用
yield QQmusic
# 在测试会话结束后自动执行
QQmusic.close() # 关闭 QQ 音乐应用
我们测试一下,创建一个同级文件"test_01"(由于pytest的特性,此处无需导入):
python
def test01(QQMusic_app):
print("test01")
def test02(QQMusic_app):
print("test02")
运行结果:

由此结果我们可以知道:close() 是在所有测试用例都跑完之后才执行的,不是每个用例跑完就关。
日志
日志文件详细讲解在这里:接口自动化测试实战项目_接口测试完整项目-CSDN博客。我们此处不讲基础语法,但依旧会详细讲解我们写代码的思路。
日志的几点要素
| 需求 | 说明 |
|---|---|
| 1. 能记录不同严重程度的日志 | 调试信息、普通流程、警告、错误要分开,方便过滤查看。 |
| 2. 日志能自动按天切分 | 每天生成新文件,避免单个文件过大,也方便归档清理。 |
| 3. 错误日志能单独拎出来 | 测试跑完,直接看 error.log 就能快速定位失败点,不用大海捞针。 |
| 4. 同时保留完整流水 | 总日志文件保留所有记录,方便回溯完整操作路径。 |
| 5. 日志格式统一且信息丰富 | 时间、级别、模块、函数、行号都要有,出问题能马上定位代码位置。 |
| 6. 全局只配置一次 | 避免每个模块都重复打开文件、重复写配置,造成日志重复或错乱。 |
创建日志文件夹 logs
我们预期的位置是放在项目根目录下,也就是和 Utils 平级(logUtils.py是我们的日志文件),如图:

如果我们直接创建,则是在当前目录下创建的,所以我们应该先向前一级,再去创建,代码如下:
python
import os
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists("../logs"):
os.mkdir("../logs")
日志输出到文件
使日志可以输出到文件,详细注释已经写在代码中了:
python
import logging
import os
import time
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists("../logs"):
os.mkdir("../logs")
#定义文件的日期
today = time.strftime("%Y-%m-%d")
#创建logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
#添加文件处理器
handler_all = logging.FileHandler(f"../logs/{today}.log",encoding='utf-8')
#把处理器添加到 Logger 上
logger.addHandler(handler_all)
logger.info("程序启动")
logger.error("程序启动")
日志分流
将日志分为 all(debug级别),error(错误级别),info(操作级别)三种日志,方便我们查看:
python
import logging
import os
import time
#定义操作级过滤器,语法记住即可
class InfoFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.INFO
#定义错误级过滤器
class ErrFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.ERROR
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists("../logs"):
os.mkdir("../logs")
#定义文件的日期
today = time.strftime("%Y-%m-%d")
#创建logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
#添加文件处理器
handler_all = logging.FileHandler(f"../logs/{today}.log",encoding='utf-8')
handler_info=logging.FileHandler(f"../logs/{today}_info.log",encoding='utf-8')
handler_info.addFilter(InfoFilter())
handler_error=logging.FileHandler(f"../logs/{today}_err.log",encoding='utf-8')
handler_error.addFilter(ErrFilter())
#把处理器添加到 Logger 上
logger.addHandler(handler_all)
logger.addHandler(handler_info)
logger.addHandler(handler_error)
logger.info("程序启动")
logger.error("程序启动")
定义日志的格式
定义日志的格式,使日志信息中可以清楚看到是什么时候,什么等级,以及哪个文件,哪个函数出的问题:
python
import logging
import os
import time
#定义操作级过滤器,语法记住即可
class InfoFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.INFO
#定义错误级过滤器
class ErrFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.ERROR
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists("../logs"):
os.mkdir("../logs")
#定义文件的日期
today = time.strftime("%Y-%m-%d")
#创建logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
#添加文件处理器
handler_all = logging.FileHandler(f"../logs/{today}.log",encoding='utf-8')
handler_info=logging.FileHandler(f"../logs/{today}_info.log",encoding='utf-8')
handler_info.addFilter(InfoFilter())
handler_error=logging.FileHandler(f"../logs/{today}_err.log",encoding='utf-8')
handler_error.addFilter(ErrFilter())
#定义日志文件的格式
fmt = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s")
handler_all.setFormatter(fmt)
handler_info.setFormatter(fmt)
handler_error.setFormatter(fmt)
#把处理器添加到 Logger 上
logger.addHandler(handler_all)
logger.addHandler(handler_info)
logger.addHandler(handler_error)
logger.info("程序启动")
logger.error("程序启动")
每个占位符的含义给大家放在下面了:
| 占位符 | 含义 | 示例输出 |
|---|---|---|
%(asctime)s |
日志被创建的时间(默认格式如 2025-06-30 14:23:45,123) |
2025-06-30 14:23:45,123 |
%(levelname)s |
日志级别(DEBUG、INFO、WARNING、ERROR、CRITICAL) | INFO |
[%(name)s] |
Logger 的名称(通常是你传给 getLogger(__name__) 的模块名) |
[qqmusic_test] |
[%(filename)s |
产生日志的源文件名(不含路径) | [main.py |
(%(funcName)s |
产生日志的函数名 | (launch |
:%(lineno)d] |
产生日志的代码行号 | :42] |
- %(message)s |
日志正文,即你调用 logger.info("xxx") 时传入的字符串 |
- 程序启动 |
封装日志
封装日志类的目的:让全项目共享同一套日志配置,做到"一次配置,到处使用;一处修改,全局生效"。
我们来看一下代码,一些新的知识点我在代码下面讲解:
python
import logging
import os
import time
#定义操作级过滤器,语法记住即可
class InfoFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.INFO
#定义错误级过滤器
class ErrFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.ERROR
#定义QQ音乐日志类
class QQMusicLogger:
logger=None
@classmethod
def getLogger(cls):
#如果没有logger,创建logger
if cls.logger is None:
cls.logger = logging.getLogger(__name__)
cls.logger.setLevel(logging.DEBUG)
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists("../logs"):
os.mkdir("../logs")
#定义文件的日期
today = time.strftime("%Y-%m-%d")
#添加文件处理器
handler_all = logging.FileHandler(f"../logs/{today}.log",encoding='utf-8')
handler_info=logging.FileHandler(f"../logs/{today}_info.log",encoding='utf-8')
handler_info.addFilter(InfoFilter())
handler_error=logging.FileHandler(f"../logs/{today}_err.log",encoding='utf-8')
handler_error.addFilter(ErrFilter())
#定义日志文件的格式
fmt = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s")
handler_all.setFormatter(fmt)
handler_info.setFormatter(fmt)
handler_error.setFormatter(fmt)
#把处理器添加到 Logger 上
cls.logger.addHandler(handler_all)
cls.logger.addHandler(handler_info)
cls.logger.addHandler(handler_error)
return cls.logger
** @classmethod :让方法属于类本身,可以直接通过类名调用,第一个参数 cls 代表类,常用于操作类变量、实现单例或工厂模式。**我们这时在其他文件调用类方法,可以这样写:
python
# 导入 QQMusicLogger 类
from utils.logger import QQMusicLogger
def test_case_01():
# 通过类名直接调用类方法,获取全局唯一的 logger
log = QQMusicLogger.getLogger()
log.info("测试用例1开始执行")
def test_case_02():
log = QQMusicLogger.getLogger() # 再次调用,返回的是同一个 logger,不会重复配置
log.error("测试用例2执行失败!")
类变量 logger 为什么能实现"全局共享"?
logger = None 是定义在 class Logger: 下面的,属于类变量 。类变量有个特点:不管你从这个类创建多少个实例,或者压根不创建实例,类变量都只有一份。
python
class Logger:
logger = None # 类变量,全项目唯一
# 即使这样用:
log1 = Logger.getlog() # 内部访问 cls.logger
log2 = Logger.getlog() # 内部访问同一个 cls.logger
print(log1 is log2) # True,是同一个对象
启动关闭程序及日志文件的调整
这波调整主要是实现可以根据不同操作自动向日志文件输入内容,我们引入日志文件的对象与方法,添加新的提示语句,并将原本输入到控制台的语句改为上输入到日志文件:
python
import pytest
import time
from pywinauto import Desktop
from pywinauto import Application
from Utils.logUtils import QQMusicLogger
class QQMusicApp:
def __init__(self):
self.win = None # 用于保存找到的主窗口对象
self.logger = QQMusicLogger.getLogger() #引入日志文件中的对象与方法
def launch(self):
"""启动 QQ 音乐并定位主窗口"""
qq_path = r"D:\QQMusicCache\QQMusicLyricNew\QQMusic\QQMusic.exe"
Application(backend='uia').start(qq_path)
# 2. 创建桌面对象,用于搜索所有顶层窗口
desktop = Desktop(backend="uia")
# 3. 轮询等待主窗口出现(最多尝试30次,每次间隔1秒)
for i in range(30):
# 列表推导式:从所有桌面窗口里筛选出满足条件的窗口
# 条件1:类名为 "TXGuiFoundation"
# 条件2:窗口当前可见
qq_windows = [win for win in desktop.windows()
if win.class_name() == "TXGuiFoundation" and win.is_visible()]
# 如果有符合条件的窗口:
if qq_windows:
#向日志输出info信息
self.logger.info("成功打开QQ音乐")
#取第一个(通常只有一个)
self.win = qq_windows[0]
self.logger.info(f">>> 找到主窗口,当前标题:{self.win.window_text()}")
return # 成功,退出方法
else:
# 还没找到,等1秒再试
time.sleep(1)
# 如果30秒后仍未找到,抛出异常
self.logger.error("错误:30秒内未找到 QQ 音乐主窗口")
raise Exception("错误:30秒内未找到 QQ 音乐主窗口")
def close(self):
"""关闭主窗口"""
if self.win:
self.win.close()
self.logger.info(">>> 窗口已关闭")
else:
self.logger.error(">>> 没有可关闭的窗口")
@pytest.fixture(scope="session")
def QQMusic_app():
# 准备工作 (Setup)
QQmusic = QQMusicApp() # 创建 QQMusicApp 类的实例
QQmusic.launch() # 启动 QQ 音乐应用
# 将准备好的对象返回给测试用例使用
yield QQmusic
# 在测试会话结束后自动执行
QQmusic.close() # 关闭 QQ 音乐应用
记得去test文件运行,因为我们修改以后,启动关闭程序是自动在用例执行前运行,无法直接运行。我们来看一下运行结果:

打开了,也关闭了,可是日志文件怎么没有变化???
这是由于 logUtils.py 的 "../logs" 是相对于当前 Python 进程的工作目录 ,而不是相对于 logUtils.py 文件所在的目录。我们将相对路径修改为绝对路径就好了:
python
import logging
import os
import time
#定义操作级过滤器,语法记住即可
class InfoFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.INFO
#定义错误级过滤器
class ErrFilter(logging.Filter):
def filter(self, record):
return record.levelno == logging.ERROR
#定义QQ音乐日志类
class QQMusicLogger:
logger=None
@classmethod
def getLogger(cls):
#如果没有logger,创建logger
if cls.logger is None:
cls.logger = logging.getLogger(__name__)
cls.logger.setLevel(logging.DEBUG)
#首先确保存放日志文件的文件夹"logs"存在,否则创建
if not os.path.exists(rf"C:\Users\lenovo\Desktop\QQMusicAuto\logs"):
os.mkdir(rf"C:\Users\lenovo\Desktop\QQMusicAuto\logs")
#定义文件的日期
today = time.strftime("%Y-%m-%d")
#添加文件处理器
handler_all = logging.FileHandler(rf"C:\Users\lenovo\Desktop\QQMusicAuto\logs\{today}.log",encoding='utf-8')
handler_info=logging.FileHandler(rf"C:\Users\lenovo\Desktop\QQMusicAuto\logs\{today}_info.log",encoding='utf-8')
handler_info.addFilter(InfoFilter())
handler_error=logging.FileHandler(rf"C:\Users\lenovo\Desktop\QQMusicAuto\logs\{today}_err.log",encoding='utf-8')
handler_error.addFilter(ErrFilter())
#定义日志文件的格式
fmt = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s")
handler_all.setFormatter(fmt)
handler_info.setFormatter(fmt)
handler_error.setFormatter(fmt)
#把处理器添加到 Logger 上
cls.logger.addHandler(handler_all)
cls.logger.addHandler(handler_info)
cls.logger.addHandler(handler_error)
return cls.logger
搞定!
pytest.ini文件配置
pytest.ini 是 pytest 的主配置文件 ,用于为整个测试项目定义默认运行规则 。它的核心作用是:让你不用每次都在命令行敲一长串参数,而是把固定配置写进文件,pytest 启动时自动读取生效。
这是我们的 pytest.ini 文件:
python
[pytest]
addopts=-vs -p no:faulthandler
| 配置片段 | 作用 | 通俗解释 |
|---|---|---|
-v |
详细输出 | 让你看清每个测试用例的名字和结果。 |
-s |
不捕获输出 | 让 print 能实时打印到控制台,方便调试。 |
-p no:faulthandler |
禁用 faulthandler 插件 |
阻止底层崩溃时打印大量底层错误堆栈,保持控制台清爽。 |
因此,addopts = -vs -p no:faulthandler 这一行配置,就是为了让 pytest 输出更详细、调试更方便,同时屏蔽掉可能干扰视线的底层 COM 错误日志,非常适合你当前正在做的 QQ 音乐 GUI 自动化测试项目。
配置 pytest.ini 前:

这并不是报错,而是"Windows 底层通信超时:被呼叫的 COM 组件没有在规定时间内响应。"简单说就是:你的 Python 脚本向 QQ 音乐的 UI 自动化接口(COM 组件)喊话:"喂,把你的窗口列表给我!"结果 QQ 音乐那边正在忙(初始化界面、加载资源),没来得及回答,Windows 就判定这次通话失败,抛出这个异常码。
配置 pytest.ini 文件后:

测试公共模块部分
好了,我们开始正式写我们的测试用例。
搜索框测试
先看一下这个窗口的属性:

这个可用于查询的属性有点少,名字为空,类名为空。只有控制类型"Edit":
python
all_edits = QQMusic_app.win.descendants(control_type="Edit")
这个语句是查询控制类型为"Edit"(输入框)的窗口,返回值为一个数组。
我们先来看一下有几个"Edit"(输入框)窗口:
python
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 打印出来看看有几个,以及它们的位置
for i, edit in enumerate(all_edits):
rect = edit.rectangle()
print(f"Edit {i}: 位置=({rect.left}, {rect.top}, {rect.right}, {rect.bottom})")
运行结果:

只有一个,那 all_edits[0] 就是我们要的窗口了。
我们对这个窗口执行如下操作:
python
def test_logo(self, QQMusic_app):
# 获取主窗口下所有 Edit 控件
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
#点击"Edit"窗口
search_edit.click_input()
#先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
#输入内容"邓紫棋"并按下回车键
search_edit.type_keys("邓紫棋{ENTER}")
time.sleep(1)
之后就是断言操作,判断结果是否符合预期了。思路是什么呢?看一下搜索出来的结果里面有没有歌手是"邓紫棋"的。

详细注释的代码:
python
def test_logo(self, QQMusic_app):
# 获取主窗口下所有 Edit 控件
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
#点击"Edit"窗口
search_edit.click_input()
#先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
#输入内容"邓紫棋"并按下回车键
search_edit.type_keys("邓紫棋{ENTER}")
time.sleep(1)
#all_result查找所有控制类型为链接的窗口(因为歌手名字其实是一个超链接,点击会跳转到歌手页面)
all_result=QQMusic_app.win.descendants(control_type="Hyperlink")
#result筛选出来这些链接中包含"邓紫棋"的结果
result=[link for link in all_result if re.search("邓紫棋",link.window_text())]
#如果result结果不为0,则测试成功
assert len(result)>0, "搜索结果中未找到包含'邓紫棋'的条目"
我们再来测试一个反例,如果我们搜索与歌曲/歌手不相干的信息,会出现此界面:

我们写一下代码,其实挺好写的,大家可以自己动手尝试(反例有很多,大家可以自行添加):
python
def test_search_fail(self,QQMusic_app):
# 获取主窗口下所有 Edit 控件
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
# 点击"Edit"窗口
search_edit.click_input()
# 先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
# 输入内容"10203344"并按下回车键
search_edit.type_keys("10203344{ENTER}")
time.sleep(1)
# all_result查找所有控制类型为链接的窗口
all_result = QQMusic_app.win.descendants(title="输入的关键词是否有误或过长")
# 如果result结果不为0,则测试成功
assert len(all_result) > 0, "并未给出错误输入提示"
窗口最小化测试
is_minimized() 是 pywinauto 窗口对象自带的方法,用于检测窗口是不是最小化不需要额外导入其他库,直接用就行:
python
def test_window_mini(self,QQMusic_app):
#找到最小化窗口
all_result = QQMusic_app.win.descendants(title="最小化",control_type="Button")
result=all_result[0]
#点击最小化按钮
result.click_input()
#判断窗口目前是否是最小化状态
assert QQMusic_app.win.is_minimized()
#还原窗口
QQMusic_app.win.restore()
常用方法给大家放在下面了:
| 方法 | 作用 | 返回值 |
|---|---|---|
is_minimized() |
判断是否最小化 | True / False |
is_maximized() |
判断是否最大化 | True / False |
is_normal() |
判断是否正常大小(既非最小也非最大) | True / False |
minimize() |
最小化窗口 | 无 |
maximize() |
最大化窗口 | 无 |
restore() |
恢复正常大小 | 无 |
get_show_state() |
获取窗口显示状态编号 | 0=正常, 1=最大化, 2=最小化 |
窗口最大化测试
和窗口最小化逻辑一样,这里就不赘述:
python
def test_window_maxi(self, QQMusic_app):
# 找到最大化窗口
all_result = QQMusic_app.win.descendants(title="最大化", control_type="Button")
result = all_result[0]
# 点击最大化按钮
result.click_input()
# 判断窗口目前是否是最大化状态
assert QQMusic_app.win.is_maximized()
# 还原窗口
QQMusic_app.win.restore()
音乐导入测试
老规矩,先找窗口(本地有歌曲也不影响,控件还是存在的,只是不在这个位置了而已):

这里需要特别注意一点:我们在主页面的时候,其实这个添加按钮是没有被激活的,只有点击"本地和下载"的时候,才会有这个按钮。因此要先点击"本地和下载"再搜索。不过这个按钮也是在主窗口下的,所以我们依然可以用win.descendants进行搜索。

先点击这个窗格:
python
# 1. 点击"本地和下载"
all_local = QQMusic_app.win.descendants(title="本地和下载", control_type="Pane")
all_local[0].click_input()
time.sleep(1)
点击添加:
python
# 2. 点击"添加"按钮
all_result = QQMusic_app.win.descendants(title="添加", control_type="Button")
all_result[0].click_input()
time.sleep(0.5)
选中这个手动添加歌曲,按下回车,会跳转到文件页面:

python
# 3. 键盘选择"手动添加歌曲"(如果你坚持用键盘,也可替换为坐标)
send_keys("{DOWN}")
time.sleep(0.2)
send_keys("{ENTER}")
获取到这个窗口:

python
# 4. 等待对话框作为主窗口的子控件出现
time.sleep(2)
# 直接从主窗口查找对话框(使用 class_name 更准)
open_dlg_list = QQMusic_app.win.descendants(title="打开", control_type="Window")
open_dlg=open_dlg_list[0]
time.sleep(1)
我们直接向输入框中输入文件路径,然后点击"打开":

python
# 5. 输入文件路径
edit_list = open_dlg.descendants(title="文件名(N):", control_type="Edit")
edit=edit_list[0]
time.sleep(0.3)
edit.click_input()
time.sleep(0.3)
edit.type_keys("^a")
edit.type_keys(r"C:\Users\lenovo\Desktop\QQMusicAuto\Music\M800001ASMC447Mslm.mp3")
# 6. 点击"打开"按钮
open_btn_list = open_dlg.descendants(title="打开(O)", control_type="Button")
open_btn=open_btn_list[0]
open_btn.click_input()
这时已经添加完成:

我们再将新增的这首歌名称和我们预期的对比,确保新增成功并且歌名符合预期:
python
# 7. 验证是否添加成功
music_name_list=QQMusic_app.win.descendants(title="童话镇",control_type="Hyperlink")
music_name=music_name_list[0]
#看一下有没有名字为"童话镇的歌曲"
assert "童话镇" in music_name.window_text(), f"未找到'童话镇',实际为'{music_name.window_text()}'"
运行一下没有问题,但是我们发现,运行第二遍就出现了问题!
这是因为我们的添加按钮的控件发生了改变,并且我们可能后续会遇到重复添加的问题!
这里的功能补充大家自行实现,博主这里就不补充了(想偷懒的作者内心OS:一会儿我再添加个删除,给这个删了,下次运行就正常了。不错不错!)

导入的音乐删除测试
这个功能这里我设计的较为简单,只是找到刚刚的哪个歌曲并删除:
我们找到歌曲后,发送单机右键,会弹出选项卡,通过发送键盘操作:

代码如下:
python
def test_import_delete(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:删除歌曲 ==========")
music_name_list = QQMusic_app.win.descendants(title="童话镇", control_type="Hyperlink")
music_name = music_name_list[0]
music_name.right_click_input()
QQMusic_app.logger.info("右键点击歌曲'童话镇',弹出菜单")
# 应对弹出的选项卡
for i in range(3):
send_keys("{UP}")
send_keys("{ENTER}")
send_keys("{ENTER}")
QQMusic_app.logger.info("键盘选择'删除'并确认")
time.sleep(2)
music_find_list=QQMusic_app.win.descendants(title="童话镇", control_type="Hyperlink")
# 如果能找到"童话镇"的歌曲,证明已删除
assert len(music_find_list) == 0, "删除失败,歌曲依然存在"
QQMusic_app.logger.info("验证通过:歌曲已删除,显示'没有本地歌曲'")
QQMusic_app.logger.info("========== 测试完成:删除歌曲 ==========")
随机播放功能测试(图像识别讲解)
这里的逻辑是,我们选择到随机播放后找到一个歌单,我这里选择了一个说书的,因为有集数,更容易判断顺序:
我们先来到这个页面,难度不大,和前面我们写的差不多:

python
# 这段部分就是找搜索框,和前面的差不多
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
# 点击"Edit"窗口
search_edit.click_input()
# 先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
# 输入内容"蛊真人|全网更新最快|多人有声"并按下回车键
search_edit.type_keys("蛊真人|全网更新最快|多人有声{ENTER}")
# 点击"专辑"选项
time.sleep(2)
album_list=QQMusic_app.win.descendants(title="专辑",control_type="Button")
album=album_list[0]
album.click_input()
time.sleep(1)
# 找到"蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更"并点击
all_novel=QQMusic_app.win.descendants(title="蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更"
,control_type="Hyperlink")
novel=all_novel[0]
novel.click_input()
下面我们需要先选择到随机播放:

没错红框框住这个,我们打开uispy后,惊讶的发现,没有 Name、没有 AutomationId、ClassName 也为空,ControlType 只是一个通用的 Pane。这意味着它完全无法通过 pywinauto 的标准属性进行定位,因为它根本没有向 UI Automation 框架暴露自己的身份。

这是典型的自绘按钮,我们需要用图像识别法来操作:
图像识别就是让程序"用眼睛看屏幕",而不是"去后台查户口"。
控件识别 vs 图像识别
| 对比项 | 控件识别(pywinauto) |
图像识别(pyautogui) |
|---|---|---|
| 原理 | 通过 UI Automation 接口,直接读取程序的"控件户口本",拿到按钮的名字、ID、坐标。 | 截取整个屏幕(或窗口),在像素矩阵里搜索和你提供的图片最相似的一块区域。 |
| 依赖 | 程序必须把控件信息注册到系统中。 | 只依赖屏幕显示内容,不关心程序内部如何实现。 |
| 优点 | 速度快、稳定、能读取文本内容、不受窗口遮挡或分辨率影响。 | 无视任何自绘、跨平台框架、游戏界面。只要肉眼能看到,它就能点。 |
| 缺点 | 遇到自绘控件、Web 内嵌页面直接"瞎"。 | 速度稍慢、依赖屏幕分辨率/DPI/主题颜色、窗口被遮挡时会失败、无法读取文字。 |
图像识别具体怎么做?
以
pyautogui为例,核心就三步:
提前截图 :把你要点击的按钮单独截一张小图,保存为
button.png(只截按钮,不要截背景)。让程序找图 :调用
pyautogui.locateCenterOnScreen('button.png'),它会返回图片在屏幕上出现时的中心坐标(x, y)。点击坐标 :
pyautogui.click(x, y)。
因为我们下面还要测试其他的播放模式,因此我们封装一个函数,自动识别到播放模式按钮并执行单击(图片为这四张图):

python
# 点击播放模式的按钮
def click_play_mode_icon():
"""尝试匹配多种播放模式图标,点击第一个找到的"""
icon_images = [
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\radom.png', # 随机播放时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\cycle.png', # 列表循环时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\one_cycle.png', # 单曲循环时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\sequence.png' # 顺序播放时的图标
]
for img_path in icon_images:
try:
x, y = pyautogui.locateCenterOnScreen(img_path, confidence=0.8)
pyautogui.click(x, y)
return True
except pyautogui.ImageNotFoundException:
continue
raise Exception("未找到任何播放模式图标,请检查")
然后选择随机播放(图片是这张)并点击:


python
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
这里我们逻辑就是,点击"下一首",看一下是第几集,如果连续三次都是顺序播放,我们判断为随机播放存在错误(有误判概率,但仅为0.1%)
python
time.sleep(2)
next_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title1 = QQMusic_app.win.window_text()
match = re.search(r'第(\d+)集', current_title1)
num1 = match.group(1) # 直接拿到数字
if num1 == '2':
# 点击"下一首"
time.sleep(2)
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title1 = QQMusic_app.win.window_text()
match1 = re.search(r'第(\d+)集', current_title1)
num2 = match1.group(1) # 直接拿到数字
if num2 == '3':
# 点击"下一首"
time.sleep(2)
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title2 = QQMusic_app.win.window_text()
match2 = re.search(r'第(\d+)集', current_title2)
num3 = match2.group(1) # 直接拿到数字
if num3 == '4':
assert False,"随机播放出现问题"
完整代码:
python
# 点击播放模式的按钮
def click_play_mode_icon():
"""尝试匹配多种播放模式图标,点击第一个找到的"""
icon_images = [
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\radom.png', # 随机播放时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\cycle.png', # 列表循环时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\one_cycle.png', # 单曲循环时的图标
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\sequence.png' # 顺序播放时的图标
]
for img_path in icon_images:
try:
x, y = pyautogui.locateCenterOnScreen(img_path, confidence=0.8)
pyautogui.click(x, y)
return True
except pyautogui.ImageNotFoundException:
continue
raise Exception("未找到任何播放模式图标,请检查")
class TestCommon:
def test_radom(self, QQMusic_app):
# 这段部分就是找搜索框,和前面的差不多
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
# 点击"Edit"窗口
search_edit.click_input()
# 先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
# 输入内容"蛊真人|全网更新最快|多人有声"并按下回车键
search_edit.type_keys("蛊真人|全网更新最快|多人有声{ENTER}")
time.sleep(1)
# 点击"专辑"选项
album_list = QQMusic_app.win.descendants(title="专辑", control_type="Button")
album = album_list[0]
album.click_input()
time.sleep(1)
# 找到"蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更"并点击
all_novel = QQMusic_app.win.descendants(title="蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更"
, control_type="Hyperlink")
novel = all_novel[0]
novel.click_input()
time.sleep(3)
# 切换模式到随机播放
click_play_mode_icon()
time.sleep(2)
# 找到选项列表的随机播放
judge_radom = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_radom.png"
x, y = pyautogui.locateCenterOnScreen(judge_radom, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
# 点击播放全部,从第一首开始放
# 这里加载慢,我们等久一点
time.sleep(1)
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
# 在屏幕上查找目标图片,返回图像中心点坐标
# confidence=0.8 表示匹配度要求为80%,可根据实际情况调整
# 注意:confidence参数需要安装opencv-python库
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
循环播放功能测试
直接点击"播放全部",从第一集开始播放,连续9次按顺序播放,则测试通过(这个播到最后一集应该会暂停并将音乐标题改为"QQ音乐,听我想听",但是后面的需要vip,因此这里不测试暂停功能,有条件的同学自己做一下)。逻辑不难,需要讲的地方不多,我帮同学们写了注释,大家自己看一下:
python
def test_cycle(self, QQMusic_app):
# 点击"播放全部",从第一集播放
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
click_play_mode_icon()
time.sleep(2)
judge_radom = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_sequence.png"
x, y = pyautogui.locateCenterOnScreen(judge_radom, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
for i in range(9):
time.sleep(1)
# 判断
match = re.search(r'第(\d+)集', QQMusic_app.win.window_text())
if i+1 != int(match.group(1)):
assert False,"顺序播放出现问题"
#点击"下一首"
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
单曲循环播放测试
这一部分不难,前面讲好多次了,切换到单曲模式后的部分着重讲一下:
python
# 点击播放全部
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
# 切换到单曲循环模式
click_play_mode_icon()
time.sleep(2)
judge_radom = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_one_cycle.png"
x, y = pyautogui.locateCenterOnScreen(judge_radom, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
title1 = QQMusic_app.win.window_text()
切换模式后呢,我们点击"下一首"其实还是会切换到第二集的。这里的单曲循环指的是,进度条到末尾后,会重播正在听的,因此我们需要将进度条拉到最后。

可是这个控件是自绘的,而我们用图像识别技术定位这个控件点击的话,大概率是直接点击到进度条中间的,难道我们要等好几分钟吗?
我这里给的解决方式是:点击"播放全部",切换至第一集,然后根据13:33图像定位。点击时向左偏移43像素,就刚好是进度条末尾:

python
time.sleep(3)
# 点击13:33(第一集结束时间)向左43的位置
time_end_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\time_end.png'
x, y = pyautogui.locateCenterOnScreen(time_end_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x-43, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
time.sleep(2)
title2 = QQMusic_app.win.window_text()
if title1!=title2:
assert None,"单曲循环错误"
大家看明白逻辑后结合代码理一下思路,以下是完整代码:
python
def test_one_cycle(self, QQMusic_app):
# 点击播放全部
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
# 切换到单曲循环模式
click_play_mode_icon()
time.sleep(2)
judge_radom = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_one_cycle.png"
x, y = pyautogui.locateCenterOnScreen(judge_radom, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
title1 = QQMusic_app.win.window_text()
time.sleep(3)
# 点击13:33(第一集结束时间)向左43的位置
time_end_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\time_end.png'
x, y = pyautogui.locateCenterOnScreen(time_end_image, confidence=0.8)
# 移动鼠标到目标位置并点击
pyautogui.moveTo(x-43, y, duration=0.5) # 0.5秒内平滑移动,便于观察
pyautogui.click()
time.sleep(2)
title2 = QQMusic_app.win.window_text()
if title1!=title2:
assert None,"单曲循环错误"
# 按个暂停
stop_list=QQMusic_app.win.descendants(title="暂停",control_type="Button")
stop=stop_list[0]
stop.click_input()
由于vip问题,列表循环我们这里不写了,逻辑都是差不多的。就是在顺序播放的那里添加一个自动跳回第一集的判断。大家有条件的话自行尝试。
音量调节功能测试
这个思路很简单:找到音量并控件上下拖动。
我们这里的音乐按钮还是自绘控件,没关系,我们来学习一下图像识别后拖拽操作(注意:拖动方法由于pyautogui无法找到图片位置,因此我们更换了方法,博客写上了就不删除了,还是有挺多地方可以用得到的,着急的小伙伴可自行跳过):
拖拽的基本方法
| 方法 | 作用 | 参数示例 |
|---|---|---|
pyautogui.dragTo(x, y, duration=0.5) |
将鼠标拖拽到 屏幕的绝对坐标 (x, y) |
目标坐标、持续时间(秒) |
pyautogui.dragRel(x_offset, y_offset, duration=0.5) |
将鼠标从当前位置拖拽相对偏移量 | 水平/垂直偏移量、持续时间 |
注意 :拖拽操作默认使用鼠标左键 。如果想用右键/滚轮用 button='right' 或 button='middle'。
假设你已经通过图像识别找到了滑块的中心坐标 (thumb_x, thumb_y),想把它拖到进度条右侧 300 像素的位置:
python
import pyautogui
# 先找到滑块
thumb = pyautogui.locateCenterOnScreen('thumb.png', confidence=0.8)
if thumb:
# 移动到滑块并拖拽到右侧 300 像素处
pyautogui.moveTo(thumb[0], thumb[1], duration=0.2)
pyautogui.dragRel(300, 0, duration=0.5) # 水平拖拽300像素
参数含义
| 方法 | 参数 | 含义 | 示例值说明 |
|---|---|---|---|
moveTo(x, y, duration) |
x |
目标点的屏幕横坐标(从左到右,0 是屏幕最左边) | thumb[0] 是滑块中心在屏幕上的 X 坐标,比如 800 |
y |
目标点的屏幕纵坐标(从上到下,0 是屏幕最顶部) | thumb[1] 是滑块中心在屏幕上的 Y 坐标,比如 600 |
|
duration |
鼠标移动到目标点所用的秒数 | 0.2 表示 0.2 秒内平滑移动过去,不是瞬间闪现 |
|
dragRel(x_offset, y_offset, duration) |
x_offset |
水平方向拖拽的距离(像素)。正数向右,负数向左 | 300 表示按住鼠标左键向右拖动 300 像素 |
y_offset |
垂直方向拖拽的距离(像素)。正数向下,负数向上 | 0 表示垂直方向不动 |
|
duration |
拖拽动作持续的秒数 | 0.5 表示 0.5 秒内完成拖拽,让程序看起来像人在操作 |
学会这个了也没什么难的了,我们来看QQ音乐的实操:

本来是打算这样写的,但是 pyautogui 找不到拖动的那个点,大家可以看一下,说不定可以在别的地方用到:
python
def test_volume(self, QQMusic_app):
# 单击音乐调节按钮
volume_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\volume.png'
vx, vy = pyautogui.locateCenterOnScreen(volume_image, confidence=0.8)
pyautogui.moveTo(vx, vy, duration=0.5)
pyautogui.click()
time.sleep(0.5)
# 找到音量滑块节点
node_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\node.png'
nx, ny = pyautogui.locateCenterOnScreen(node_image, confidence=0.5)
pyautogui.moveTo(nx, ny, duration=0.5)
pyautogui.dragRel(0, -50, duration=0.5) # 向上拖拽增大音量
pyautogui.dragRel(0, 50, duration=0.5) # 向下拖拽减小音量
我们第一次方案后失败后,给出的第二个方案是:以小喇叭图案为参照,点击音量条的不同位置调节音量:
python
# 找到音量条的位置并点击不同位置
x,y=pyautogui.locateCenterOnScreen(volume_image, confidence=0.8)
# 静音
pyautogui.moveTo(x+5,y-100, duration=0.5)
pyautogui.click()
# 调高到33%
time.sleep(1)
pyautogui.moveTo(x + 5, y - 250, duration=0.5)
pyautogui.click()
# 调高到75%
time.sleep(1)
pyautogui.moveTo(x + 5, y - 320, duration=0.5)
pyautogui.click()
最后添加断言,幸运的是,我们在 UIspy 上发现这个音量的值可以直接获取到,那咱们直接开写:

代码如下:
python
def test_volume(self, QQMusic_app):
# 单击音乐调节按钮
volume_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\volume.png'
pyautogui.moveTo(volume_image, duration=0.5)
pyautogui.click()
time.sleep(0.3)
x, y = pyautogui.locateCenterOnScreen(volume_image, confidence=0.8)
# 调高到33%~35%
pyautogui.moveTo(x + 5, y - 252, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
vol_list = [btn for btn in all_btns if re.search(r'音量:\d+%', btn.window_text())]
vol = vol_list[0]
vol_num = re.search(r'\d+', vol.window_text()).group()
assert 33 <= int(vol_num) <= 35
# 调高到75%
time.sleep(0.3)
pyautogui.moveTo(x + 5, y - 320, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
vol_list = [btn for btn in all_btns if re.search(r'音量:\d+%', btn.window_text())]
vol = vol_list[0]
vol_num = re.search(r'\d+', vol.window_text()).group()
assert 74 <= int(vol_num) <= 76
# 静音
time.sleep(0.3)
pyautogui.moveTo(x + 5, y - 100, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
mute_list = [btn for btn in all_btns if re.search(r'静音', btn.window_text())]
mute = mute_list[0]
mute_text = mute.window_text()
assert "静音" in mute_text
这里说一下:vol_match 是包含匹配信息的"盒子",vol_match.group() 是从盒子中取出的"实物字符串"。要比较内容,必须用 .group()。
换肤功能测试
点击换肤按钮,来到换肤页面,不解释:
python
# 点击换肤按钮
skin_list = QQMusic_app.win.descendants(title="换肤", control_type="Button")
skin_list[0].click_input()
time.sleep(1)
将鼠标移动到换肤页面后滚动鼠标(否则会在状态栏无效滚动),使我们的黑白色显示出来:
python
# 2. 从当前位置向上移动500像素,并向右移动500像素
pyautogui.moveRel(500, -500, duration=0.5)
# 在当前位置向上滚动250个单位
pyautogui.scroll(-250)
time.sleep(1)
将鼠标定位到黑色,我这里用的是下图。鼠标悬停后会自动弹出"立即使用"按钮,稍微调整鼠标位置后按下:

python
# 定位到黑色并点击
skin_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\black.png'
x, y = pyautogui.locateCenterOnScreen(skin_black, confidence=0.8)
time.sleep(0.5)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
白色同理:
python
# 定位到白色并点击
skin_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\white.png'
x, y = pyautogui.locateCenterOnScreen(skin_black, confidence=0.8)
time.sleep(0.5)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
最后我们添加验证语句,如果我们将鼠标移动到白色上,而我们正在使用白色,会在原来的背景上加一个绿色的蒙版,并显示"正在使用",如下图:

我们可以由此判断黑色和白色是否正在使用。
完整代码:
python
def test_change_skin(self, QQMusic_app):
# 点击换肤按钮
skin_list = QQMusic_app.win.descendants(title="换肤", control_type="Button")
skin_list[0].click_input()
time.sleep(1)
# 2. 从当前位置向上移动500像素,并向右移动500像素
pyautogui.moveRel(500, -500, duration=0.5)
# 在当前位置向上滚动250个单位
pyautogui.scroll(-250)
time.sleep(1)
# 定位到黑色并点击
skin_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\black.png'
x, y = pyautogui.locateCenterOnScreen(skin_black, confidence=0.8)
time.sleep(0.5)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
# 验证黑色主题
judge_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_black.png'
try:
pyautogui.locateCenterOnScreen(judge_black, confidence=0.8)
except pyautogui.ImageNotFoundException:
assert False, "黑色主题验证失败,未检测到黑色主题特征"
# 定位到白色并点击
skin_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\white.png'
x, y = pyautogui.locateCenterOnScreen(skin_black, confidence=0.8)
time.sleep(0.5)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
time.sleep(1)
#验证白色主题
judge_white=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_white.png'
try:
pyautogui.locateCenterOnScreen(judge_white, confidence=0.8)
except pyautogui.ImageNotFoundException:
assert False, "白色主题验证失败,未检测到白色主题特征"
公共部分的测试就到这里了,肯定有许多没有完善的地方,期待大家的补充。
公共部分测试完整代码
这是公共部分完整的代码,并添加了日志输入语句和pytest.mark.order(1)(用于测试文件的排序):
python
import re
import time
import pyautogui
import pytest
from pywinauto.keyboard import send_keys
@pytest.mark.order(1)
def click_play_mode_icon():
"""尝试匹配多种播放模式图标,点击第一个找到的"""
icon_images = [
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\radom.png',
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\cycle.png',
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\one_cycle.png',
r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\sequence.png'
]
for img_path in icon_images:
try:
x, y = pyautogui.locateCenterOnScreen(img_path, confidence=0.8)
pyautogui.click(x, y)
return True
except pyautogui.ImageNotFoundException:
continue
raise Exception("未找到任何播放模式图标,请检查")
class TestCommon:
@pytest.mark.order(2)
def test_search_fail(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:搜索失败场景 ==========")
# 获取主窗口下所有 Edit 控件
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
# 点击"Edit"窗口
search_edit.click_input()
# 先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
# 输入内容"10203344"并按下回车键
QQMusic_app.logger.info("输入无效关键词 '10203344' 并搜索")
search_edit.type_keys("10203344{ENTER}")
time.sleep(1)
# all_result查找所有控制类型为链接的窗口
all_result = QQMusic_app.win.descendants(title="输入的关键词是否有误或过长")
# 如果result结果不为0,则测试成功
assert len(all_result) > 0, "并未给出错误输入提示"
QQMusic_app.logger.info("验证通过:显示了错误输入提示")
QQMusic_app.logger.info("========== 测试完成:搜索失败场景 ==========")
@pytest.mark.order(3)
def test_search_success(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:搜索成功场景 ==========")
# 获取主窗口下所有 Edit 控件
all_edits = QQMusic_app.win.descendants(control_type="Edit")
# 这里只有一个"Edit"(输入框)窗口
search_edit = all_edits[0]
# 点击"Edit"窗口
search_edit.click_input()
# 先ctrl+A全部选中输入框中的内容,防止输入框中本身就有内容
search_edit.type_keys("^a")
# 输入内容"邓紫棋"并按下回车键
QQMusic_app.logger.info("输入关键词 '邓紫棋' 并搜索")
search_edit.type_keys("邓紫棋{ENTER}")
time.sleep(1)
# all_result查找所有控制类型为链接的窗口(因为歌手名字其实是一个超链接,点击会跳转到歌手页面)
all_result = QQMusic_app.win.descendants(control_type="Hyperlink")
# result筛选出来这些链接中包含"邓紫棋"的结果
result = [link for link in all_result if re.search("邓紫棋", link.window_text())]
# 如果result结果不为0,则测试成功
assert len(result) > 0, "搜索结果中未找到包含'邓紫棋'的条目"
QQMusic_app.logger.info(f"验证通过:找到 {len(result)} 条包含'邓紫棋'的搜索结果")
QQMusic_app.logger.info("========== 测试完成:搜索成功场景 ==========")
@pytest.mark.order(4)
def test_window_mini(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:窗口最小化 ==========")
# 找到最小化窗口
all_result = QQMusic_app.win.descendants(title="最小化", control_type="Button")
result = all_result[0]
# 点击最小化按钮
result.click_input()
QQMusic_app.logger.info("点击最小化按钮")
# 判断窗口目前是否是最小化状态
assert QQMusic_app.win.is_minimized()
QQMusic_app.logger.info("验证通过:窗口已最小化")
# 还原窗口
QQMusic_app.win.restore()
QQMusic_app.logger.info("窗口已还原")
QQMusic_app.logger.info("========== 测试完成:窗口最小化 ==========")
@pytest.mark.order(5)
def test_window_maxi(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:窗口最大化 ==========")
# 找到最大化窗口
all_result = QQMusic_app.win.descendants(title="最大化", control_type="Button")
result = all_result[0]
# 点击最大化按钮
result.click_input()
QQMusic_app.logger.info("点击最大化按钮")
# 判断窗口目前是否是最大化状态
assert QQMusic_app.win.is_maximized()
QQMusic_app.logger.info("验证通过:窗口已最大化")
# 还原窗口
QQMusic_app.win.restore()
QQMusic_app.logger.info("窗口已还原")
QQMusic_app.logger.info("========== 测试完成:窗口最大化 ==========")
@pytest.mark.order(6)
def test_import(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:导入音乐 ==========")
# 1. 点击"本地和下载"
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list=[i for i in all_local_list if re.search("本地和下载",i.window_text())]
local_list[0].click_input()
time.sleep(1)
# 点击本地歌曲
download_list = QQMusic_app.win.descendants(control_type='Button')
download = [i for i in download_list if re.search("本地歌曲", i.window_text())]
download_Button = download[0]
download_Button.click_input()
# 2. 点击"添加"按钮
all_result = QQMusic_app.win.descendants(title="添加", control_type="Button")
all_result[0].click_input()
time.sleep(0.8)
# 3. 键盘选择"手动添加歌曲"(如果你坚持用键盘,也可替换为坐标)
send_keys("{DOWN}")
time.sleep(0.8)
send_keys("{ENTER}")
QQMusic_app.logger.info("选择'手动添加歌曲'")
# 4. 等待对话框作为主窗口的子控件出现
time.sleep(0.6)
open_dlg_list = QQMusic_app.win.descendants(title="打开", control_type="Window")
open_dlg = open_dlg_list[0]
time.sleep(0.3)
# 5. 输入文件路径
edit_list = open_dlg.descendants(title="文件名(N):", control_type="Edit")
edit = edit_list[0]
time.sleep(0.3)
edit.click_input()
time.sleep(0.3)
edit.type_keys("^a")
file_path = r"C:\Users\lenovo\Desktop\QQMusicAuto\Music\M800001ASMC447Mslm.mp3"
edit.type_keys(file_path)
QQMusic_app.logger.info(f"输入文件路径: {file_path}")
# 6. 点击"打开"按钮
open_btn_list = open_dlg.descendants(title="打开(O)", control_type="Button")
open_btn = open_btn_list[0]
open_btn.click_input()
QQMusic_app.logger.info("点击'打开'按钮")
# 7. 验证是否添加成功
music_name_list = QQMusic_app.win.descendants(title="童话镇", control_type="Hyperlink")
music_name = music_name_list[0]
assert "童话镇" in music_name.window_text(), f"未找到'童话镇',实际为'{music_name.window_text()}'"
QQMusic_app.logger.info("验证通过:歌曲'童话镇'已成功添加到列表")
QQMusic_app.logger.info("========== 测试完成:导入音乐 ==========")
@pytest.mark.order(7)
def test_import_delete(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:删除歌曲 ==========")
music_name_list = QQMusic_app.win.descendants(title="童话镇", control_type="Hyperlink")
music_name = music_name_list[0]
music_name.right_click_input()
QQMusic_app.logger.info("右键点击歌曲'童话镇',弹出菜单")
# 应对弹出的选项卡
for i in range(3):
send_keys("{UP}")
send_keys("{ENTER}")
send_keys("{ENTER}")
QQMusic_app.logger.info("键盘选择'删除'并确认")
time.sleep(2)
music_find_list=QQMusic_app.win.descendants(title="童话镇", control_type="Hyperlink")
# 如果能找到"童话镇"的歌曲,证明已删除
assert len(music_find_list) == 0, "删除失败,歌曲依然存在"
QQMusic_app.logger.info("验证通过:歌曲已删除,显示'没有本地歌曲'")
QQMusic_app.logger.info("========== 测试完成:删除歌曲 ==========")
@pytest.mark.order(8)
def test_radom(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:随机播放模式 ==========")
# 这段部分就是找搜索框,和前面的差不多
all_edits = QQMusic_app.win.descendants(control_type="Edit")
search_edit = all_edits[0]
search_edit.click_input()
search_edit.type_keys("^a")
search_edit.type_keys("蛊真人|全网更新最快|多人有声{ENTER}")
QQMusic_app.logger.info("搜索有声书'蛊真人'")
time.sleep(1)
# 点击"专辑"选项
album_list = QQMusic_app.win.descendants(title="专辑", control_type="Button")
album = album_list[0]
album.click_input()
time.sleep(1)
# 找到目标专辑并点击
all_novel = QQMusic_app.win.descendants(
title="蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更",
control_type="Hyperlink")
novel = all_novel[0]
novel.click_input()
QQMusic_app.logger.info("进入有声书详情页")
time.sleep(3)
# 切换模式到随机播放
click_play_mode_icon()
time.sleep(2)
judge_radom = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_radom.png"
x, y = pyautogui.locateCenterOnScreen(judge_radom, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("切换播放模式为'随机播放'")
# 点击播放全部
time.sleep(1)
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击'播放全部'")
# 点击"下一首"
time.sleep(2)
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title1 = QQMusic_app.win.window_text()
match = re.search(r'第(\d+)集', current_title1)
num1 = match.group(1)
QQMusic_app.logger.info(f"第一次切换后播放第 {num1} 集")
if num1 == '2':
time.sleep(2)
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title1 = QQMusic_app.win.window_text()
match1 = re.search(r'第(\d+)集', current_title1)
num2 = match1.group(1)
QQMusic_app.logger.info(f"第二次切换后播放第 {num2} 集")
if num2 == '3':
time.sleep(2)
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
current_title2 = QQMusic_app.win.window_text()
match2 = re.search(r'第(\d+)集', current_title2)
num3 = match2.group(1)
QQMusic_app.logger.info(f"第三次切换后播放第 {num3} 集")
if num3 == '4':
QQMusic_app.logger.error("连续三次顺序播放,随机模式失效")
assert False, "随机播放出现问题"
QQMusic_app.logger.info("验证通过:未出现连续三次顺序播放")
QQMusic_app.logger.info("========== 测试完成:随机播放模式 ==========")
@pytest.mark.order(9)
def test_cycle(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:顺序播放模式 ==========")
# 点击"播放全部",从第一集播放
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
click_play_mode_icon()
time.sleep(2)
judge_sequence = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_sequence.png"
x, y = pyautogui.locateCenterOnScreen(judge_sequence, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("切换播放模式为'顺序播放'")
for i in range(4):
time.sleep(0.6)
match = re.search(r'第(\d+)集', QQMusic_app.win.window_text())
current_ep = int(match.group(1))
QQMusic_app.logger.info(f"第 {i+1} 次检查:当前播放第 {current_ep} 集,期望第 {i+1} 集")
if i + 1 != current_ep:
QQMusic_app.logger.error(f"顺序播放错误:期望第 {i+1} 集,实际第 {current_ep} 集")
assert False, "顺序播放出现问题"
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next.png'
x, y = pyautogui.locateCenterOnScreen(next_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("验证通过:连续4次切换均按顺序播放")
QQMusic_app.logger.info("========== 测试完成:顺序播放模式 ==========")
@pytest.mark.order(10)
def test_one_cycle(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:单曲循环模式 ==========")
# 点击播放全部
play_all_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_all.png'
x, y = pyautogui.locateCenterOnScreen(play_all_image, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
# 切换到单曲循环模式
click_play_mode_icon()
time.sleep(2)
judge_one_cycle = r"C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_one_cycle.png"
x, y = pyautogui.locateCenterOnScreen(judge_one_cycle, confidence=0.8)
pyautogui.moveTo(x, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("切换播放模式为'单曲循环'")
title1 = QQMusic_app.win.window_text()
QQMusic_app.logger.info(f"当前播放标题: {title1}")
time.sleep(3)
# 点击进度条末尾
time_end_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\time_end.png'
x, y = pyautogui.locateCenterOnScreen(time_end_image, confidence=0.8)
pyautogui.moveTo(x - 43, y, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击进度条末尾,等待切换")
time.sleep(2)
title2 = QQMusic_app.win.window_text()
QQMusic_app.logger.info(f"切换后播放标题: {title2}")
if title1 != title2:
QQMusic_app.logger.error(f"单曲循环错误:标题由 '{title1}' 变为 '{title2}'")
assert None, "单曲循环错误"
# 按个暂停
stop_list = QQMusic_app.win.descendants(title="暂停", control_type="Button")
stop = stop_list[0]
stop.click_input()
QQMusic_app.logger.info("验证通过:单曲循环模式下未自动切歌")
QQMusic_app.logger.info("========== 测试完成:单曲循环模式 ==========")
@pytest.mark.order(11)
def test_volume(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:音量调节 ==========")
# 单击音乐调节按钮
volume_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\volume.png'
pyautogui.moveTo(volume_image, duration=0.5)
pyautogui.click()
time.sleep(0.3)
x, y = pyautogui.locateCenterOnScreen(volume_image, confidence=0.8)
# 调高到33%~35%
QQMusic_app.logger.info("正在调节音量至 34%...")
pyautogui.moveTo(x + 5, y - 252, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
vol_list = [btn for btn in all_btns if re.search(r'音量:\d+%', btn.window_text())]
vol = vol_list[0]
vol_num = re.search(r'\d+', vol.window_text()).group()
assert 33 <= int(vol_num) <= 35
QQMusic_app.logger.info(f"验证通过:音量已调节至 {vol_num}%")
# 调高到75%
QQMusic_app.logger.info("正在调节音量至 75%...")
time.sleep(0.3)
pyautogui.moveTo(x + 5, y - 320, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
vol_list = [btn for btn in all_btns if re.search(r'音量:\d+%', btn.window_text())]
vol = vol_list[0]
vol_num = re.search(r'\d+', vol.window_text()).group()
assert 74 <= int(vol_num) <= 76
QQMusic_app.logger.info(f"验证通过:音量已调节至 {vol_num}%")
# 静音
QQMusic_app.logger.info("正在切换至静音...")
time.sleep(0.3)
pyautogui.moveTo(x + 5, y - 100, duration=0.5)
pyautogui.click()
time.sleep(0.3)
all_btns = QQMusic_app.win.descendants(control_type="Button")
mute_list = [btn for btn in all_btns if re.search(r'静音', btn.window_text())]
mute = mute_list[0]
mute_text = mute.window_text()
assert "静音" in mute_text
QQMusic_app.logger.info("验证通过:已切换至静音")
QQMusic_app.logger.info("========== 测试完成:音量调节 ==========")
@pytest.mark.order(12)
def test_change_skin(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:换肤功能 ==========")
# 点击换肤按钮
skin_list = QQMusic_app.win.descendants(title="换肤", control_type="Button")
skin_list[0].click_input()
time.sleep(1)
# 2. 移动并滚动
pyautogui.moveRel(500, -500, duration=0.5)
pyautogui.scroll(-250)
time.sleep(1)
# 定位到黑色并点击
skin_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\black.png'
x, y = pyautogui.locateCenterOnScreen(skin_black, confidence=0.8)
time.sleep(0.5)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
QQMusic_app.logger.info("点击黑色主题")
# 验证黑色主题
judge_black = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_black.png'
try:
pyautogui.locateCenterOnScreen(judge_black, confidence=0.8)
QQMusic_app.logger.info("验证通过:黑色主题特征已出现")
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.error("黑色主题验证失败")
assert False, "黑色主题验证失败,未检测到黑色主题特征"
time.sleep(0.5)
# 定位到白色并点击
skin_white = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\white.png'
x, y = pyautogui.locateCenterOnScreen(skin_white, confidence=0.8)
time.sleep(1
)
pyautogui.moveTo(x - 20, y + 20)
pyautogui.click()
QQMusic_app.logger.info("点击白色主题")
time.sleep(1)
# 验证白色主题
judge_white = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_white.png'
try:
pyautogui.locateCenterOnScreen(judge_white, confidence=0.8)
QQMusic_app.logger.info("验证通过:白色主题特征已出现")
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.error("白色主题验证失败")
assert False, "白色主题验证失败,未检测到白色主题特征"
QQMusic_app.logger.info("========== 测试完成:换肤功能 ==========")
日志文件:

在线音乐模块测试
推荐页测试
推荐页面就像一个巨大的Pane画布,QQ音乐用代码在上面绘制了所有轮播图、歌单卡片、文字。对UIA来说,整个页面就是一块"像素板",没有独立的按钮或列表项。传统自动化工具因此"失明"。
我们这里主要验证下面几点:
1.推荐页可以正常加载(2、3、4通过,1自然没问题,不用专门写测试用例)
2.推荐页的推荐音乐点击播放可以切换歌曲
3.推荐页的轮播箭头功能测试
4.点击歌单后可以直接进入歌单
点击播放推荐能够切换歌曲
我们先来看看点击这个是否会换歌(点击前获取一次窗口名称,点击后获取一次,看看是否不同,不同即为切歌了):

由于下面还有一个长得一模一样的播放按键,所以我直接以左上角的"Hi"为基准点击这个播放按钮:
python
def test_play(self,QQMusic_app):
# 点击"推荐"控件,使页面来到推荐页面
recommmend_list=QQMusic_app.win.descendants(title="推荐",control_type="Pane")
recommend_button = recommmend_list[0]
recommend_button.click_input()
# 获取按播放键前的歌曲名
title1 = QQMusic_app.win.window_text()
# 以左上角的"Hi"为基准定位到播放推荐
hi_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\Hi.png'
x,y=pyautogui.locateCenterOnScreen(hi_image,confidence=0.8)
pyautogui.moveTo(x+50,y+200,duration=0.5)
pyautogui.click()
time.sleep(0.3)
title2 = QQMusic_app.win.window_text()
# 确保切歌前后歌名不一样
assert title1!=title2
推荐页的轮播箭头功能测试
这个也简单,"猜你喜欢"应该是常驻的歌单,不会随着个性化消失。我们就以"猜你喜欢"的四个字作为基准(就是那个"夜深了"图片下面两行小字中的一行),点击右箭头四个字找不到,再点一次左箭头又能够找到了:

详细注释代码:
python
def test_button(self,QQMusic_app):
next_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next_recommend.png'
last_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\last_recommend.png'
like_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_like.png'
# 点击右箭头,"猜你喜欢"会消失
pyautogui.moveTo(next_image,duration=0.5)
pyautogui.click()
time.sleep(0.5)
try:
pyautogui.locateCenterOnScreen(like_image,confidence=0.8)
assert False
except pyautogui.ImageNotFoundException:
pass
# 点击左箭头,"猜你喜欢"会重新出现
pyautogui.moveTo(last_image, duration=0.5)
pyautogui.click()
time.sleep(0.5)
try:
pyautogui.locateCenterOnScreen(like_image, confidence=0.8)
except pyautogui.ImageNotFoundException:
assert False
点击歌单可以进入
有控件信息这就是点一下控件的事情,没办法。我们来看一下用图像识别是什么逻辑:
还是以官方常驻歌单为例,这里用的是"每日30首",因为图片会变动,我们还是以文字为基准,鼠标偏移点击到图片:
python
# 点击歌单"每日30首"
image_30 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\30.png'
x, y = pyautogui.locateCenterOnScreen(image_30, confidence=0.8)
pyautogui.moveTo(x - 20, y - 50)
pyautogui.click()
time.sleep(2)
点击后进入歌单界面,判断是否出现下图框选的部分:

代码如下:
python
# 判断是否有"每日30首"的标题
judge_30_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_30.png'
try:
pyautogui.locateCenterOnScreen(judge_30_image, confidence=0.8)
except pyautogui.ImageNotFoundException:
assert False
值得一提的是,我们最后这里需要点一下歌单上的"播放",这是因为,如果不点击,我们歌曲放的是推荐那里的,播放模式会改变,如图:

最后一个问题,就是当"每日30首"的第一首是会员歌曲,会有会员弹窗,我们关闭一下:

代码实现:
python
def test_pull_down(self, QQMusic_app):
# 点击歌单"每日30首"
image_30 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\30.png'
x, y = pyautogui.locateCenterOnScreen(image_30, confidence=0.8)
pyautogui.moveTo(x - 20, y - 50)
pyautogui.click()
time.sleep(2)
# 判断是否有"每日30首"的标题
judge_30_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_30.png'
try:
pyautogui.locateCenterOnScreen(judge_30_image, confidence=0.8)
except pyautogui.ImageNotFoundException:
assert False
# 按下播放
play_30_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_30.png'
pyautogui.moveTo(play_30_image,duration=0.5)
pyautogui.click()
# 按个暂停
stop_list = QQMusic_app.win.descendants(title="暂停", control_type="Button")
stop = stop_list[0]
stop.click_input()
# 关闭会员弹窗
time.sleep(2)
close_list=QQMusic_app.win.descendants(title="儲",control_type="Button")
if len(close_list) != 0:
close = close_list[0]
close.click_input()
else:
pass
在线音乐模块完整代码
推荐页就到这里吧,乐馆和听书页面也是完全自绘的页面,逻辑差的不多,大家想完善可以继续完善。下面是我们完整的有日志输入语句的代码(这里的图片匹配度我全调为0.6了,0.8会有个别情况不通过):
python
import re
import time
import pytest
import pyautogui
from pywinauto.keyboard import send_keys
from selenium.webdriver.support.expected_conditions import title_is
class TestRecommend:
@pytest.mark.order(101)
def test_play(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:推荐页点击播放切歌 ==========")
# 点击"推荐"控件,使页面来到推荐页面
recommmend_list=QQMusic_app.win.descendants(title="推荐",control_type="Pane")
recommend_button = recommmend_list[0]
recommend_button.click_input()
# 获取按播放键前的歌曲名
title1 = QQMusic_app.win.window_text()
QQMusic_app.logger.info(f"点击播放前的歌曲标题: {title1}")
# 以左上角的"Hi"为基准定位到播放推荐
hi_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\Hi.png'
x, y = pyautogui.locateCenterOnScreen(hi_image, confidence=0.6)
pyautogui.moveTo(x + 50, y + 200, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击推荐音乐播放按钮")
time.sleep(0.5)
title2 = QQMusic_app.win.window_text()
QQMusic_app.logger.info(f"点击播放后的歌曲标题: {title2}")
# 确保切歌前后歌名不一样
assert title1 != title2
QQMusic_app.logger.info("验证通过:切歌成功,歌曲标题已改变")
QQMusic_app.logger.info("========== 测试完成:推荐页点击播放切歌 ==========")
@pytest.mark.order(102)
def test_button(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:推荐页左右箭头刷新 ==========")
next_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\next_recommend.png'
last_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\last_recommend.png'
like_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_like.png'
# 点击右箭头,"猜你喜欢"会消失
pyautogui.moveTo(next_image, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击右箭头")
time.sleep(0.5)
try:
pyautogui.locateCenterOnScreen(like_image, confidence=0.6)
QQMusic_app.logger.error("右箭头点击后'猜你喜欢'未消失")
assert False
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.info("验证通过:右箭头点击后'猜你喜欢'已消失")
# 点击左箭头,"猜你喜欢"会重新出现
pyautogui.moveTo(last_image, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击左箭头")
time.sleep(0.5)
try:
pyautogui.locateCenterOnScreen(like_image, confidence=0.6)
QQMusic_app.logger.info("验证通过:左箭头点击后'猜你喜欢'重新出现")
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.error("左箭头点击后'猜你喜欢'未出现")
assert False
QQMusic_app.logger.info("========== 测试完成:推荐页左右箭头刷新 ==========")
@pytest.mark.order(103)
def test_pull_down(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:推荐页点击歌单进入详情 ==========")
# 点击歌单"每日30首"
image_30 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\30.png'
x, y = pyautogui.locateCenterOnScreen(image_30, confidence=0.6)
pyautogui.moveTo(x - 20, y - 50)
pyautogui.click()
QQMusic_app.logger.info("点击'每日30首'歌单")
time.sleep(2)
# 判断是否有"每日30首"的标题
judge_30_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\judge_30.png'
try:
pyautogui.locateCenterOnScreen(judge_30_image, confidence=0.6)
QQMusic_app.logger.info("验证通过:成功进入'每日30首'歌单详情页")
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.error("未检测到'每日30首'标题,进入歌单失败")
assert False
# 按下播放
play_30_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\play_30.png'
pyautogui.moveTo(play_30_image, duration=0.5)
pyautogui.click()
QQMusic_app.logger.info("点击歌单播放按钮")
# 按个暂停
stop_list = QQMusic_app.win.descendants(title="暂停", control_type="Button")
stop = stop_list[0]
stop.click_input()
QQMusic_app.logger.info("点击暂停按钮")
QQMusic_app.logger.info("========== 测试完成:推荐页点击歌单进入详情 ==========")
# 关闭会员弹窗
time.sleep(2)
close_list=QQMusic_app.win.descendants(title="儲",control_type="Button")
if len(close_list) != 0:
close = close_list[0]
close.click_input()
else:
pass
我的音乐模块测试
下载音乐测试
这里逻辑也比较简单,我们的思路就是先检查一下这里下载前没有我们要下载的音乐,如果有删除即可:

然后我们还是去找蛊真人第一集下载,至于为什么不下歌曲,当然使vip问题啊。
UI自绘,我们直接找图"蛊真人 第1集",但这玩意标题有时候灰的有时候绿的,而且绿色和灰色截图位置一模一样,但绿色识别不了。因此我们直接以"歌曲为基准",定位到第一集:

代码如下:
python
# 鼠标移动到第一集
song_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\song_image.png'
x,y=pyautogui.locateCenterOnScreen(song_image)
pyautogui.moveTo(x, y+80)
鼠标悬停后,会自动弹出下载等按钮,我们图像识别后点击(别问我为什么不发送右键,第一遍忘了,写博客才想起来),这里的策略是刚刚的,直接识别箭头,但也是有时候灰色有时候绿色,我们分情况:
python
#点击下载
time.sleep(0.5)
song_download1=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download1.png'
song_download2 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download2.png'
try:
pyautogui.moveTo(song_download1)
except pyautogui.ImageNotFoundException:
try:
pyautogui.moveTo(song_download2)
except pyautogui.ImageNotFoundException:
raise Exception("两个图片都没找到")
pyautogui.click()
time.sleep(1)
music_quality=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\music_quality.png'
pyautogui.moveTo(music_quality)
pyautogui.click()
再次到"下载歌曲"页面查看,因为我们下载完毕,后面会显示数字,因此这里我们要用正则匹配,这段不难理解:
python
# 点击"本地和下载"
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list = [i for i in all_local_list if re.search("本地和下载", i.window_text())]
local_list[0].click_input()
time.sleep(1)
# 点击到下载页面
download_list = QQMusic_app.win.descendants(control_type='Button')
download = [i for i in download_list if re.search("下载歌曲", i.window_text())]
download_Button = download[0]
download_Button.click_input()
time.sleep(2)
one_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type="Hyperlink")
assert len(one_list) > 0,"下载失败"
完整代码:
python
def test_download(self,QQMusic_app):
# 1. 点击"本地和下载"
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list = [i for i in all_local_list if re.search("本地和下载", i.window_text())]
local_list[0].click_input()
time.sleep(1)
# 点击到下载页面
download_list=QQMusic_app.win.descendants(control_type='Button')
download=[i for i in download_list if re.search("下载歌曲",i.window_text())]
download_Button=download[0]
download_Button.click_input()
# 确认没有下载蛊真人第一集,如果有删除
time.sleep(1)
one_list=QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)", control_type="Hyperlink")
if len(one_list)>0:
one=one_list[0]
one.right_click_input()
# 应对弹出的选项卡
for i in range(3):
send_keys("{UP}")
send_keys("{ENTER}")
send_keys("{ENTER}")
time.sleep(2)
# 这段部分就是找搜索框,和前面的差不多
all_edits = QQMusic_app.win.descendants(control_type="Edit")
search_edit = all_edits[0]
search_edit.click_input()
search_edit.type_keys("^a")
search_edit.type_keys("蛊真人|全网更新最快|多人有声{ENTER}")
time.sleep(1)
# 点击"专辑"选项
album_list = QQMusic_app.win.descendants(title="专辑", control_type="Button")
album = album_list[0]
album.click_input()
time.sleep(1)
# 找到目标专辑并点击
all_novel = QQMusic_app.win.descendants(
title="蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更",
control_type="Hyperlink")
novel = all_novel[0]
novel.click_input()
time.sleep(5)
# 鼠标移动到第一集
song_image=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\song_image.png'
x,y=pyautogui.locateCenterOnScreen(song_image)
pyautogui.moveTo(x, y+80)
#点击下载
time.sleep(0.5)
song_download1=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download1.png'
song_download2 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download2.png'
try:
pyautogui.moveTo(song_download1)
except pyautogui.ImageNotFoundException:
try:
pyautogui.moveTo(song_download2)
except pyautogui.ImageNotFoundException:
raise Exception("两个图片都没找到")
pyautogui.click()
time.sleep(1)
music_quality=r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\music_quality.png'
pyautogui.moveTo(music_quality)
pyautogui.click()
# 点击"本地和下载"
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list = [i for i in all_local_list if re.search("本地和下载", i.window_text())]
local_list[0].click_input()
time.sleep(1)
# 点击到下载页面
download_list = QQMusic_app.win.descendants(control_type='Button')
download = [i for i in download_list if re.search("下载歌曲", i.window_text())]
download_Button = download[0]
download_Button.click_input()
time.sleep(2)
one_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type="Hyperlink")
assert len(one_list) > 0,"下载失败"
我喜欢and取消喜欢测试
我喜欢按钮控件可以直接找到:
python
# 点击"喜欢"按钮
Mylike_button_list=QQMusic_app.win.descendants(title="我喜欢",control_type="Button")
Mylike_button=Mylike_button_list[0]
Mylike_button.click_input()
进入状态栏中的"喜欢",查看刚刚添加的歌曲:
python
# 点击状态栏的"喜欢"
all_Mylike_list = QQMusic_app.win.descendants(control_type="Pane")
Mylisk_list=[i for i in all_Mylike_list if re.search("喜欢",i.window_text())]
Mylike = Mylisk_list[0]
Mylike.click_input()
# 搜索喜欢列表里有没有刚添加的歌曲
guzhenren_like_list=QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",control_type='Hyperlink')
assert len(guzhenren_like_list)>0
取消喜欢:

python
# 取消喜欢,方便下次运行
guzhenren_like=guzhenren_like_list[0]
guzhenren_like.click_input()
cancel_like_list=QQMusic_app.win.descendants(title="取消喜欢",control_type="Button")
cancel_like=cancel_like_list[0]
cancel_like.click_input()
取消时会出现弹窗:

python
#应对弹窗
time.sleep(1)
OK_list=QQMusic_app.win.descendants(title="确定",control_type="Button")
# 防止弹窗没有弹出(因为我也不确定是不是每次都会弹)
if len(OK_list)>0:
# 点击确定
OK_list[0].click_input()
断言确保歌曲已取消喜欢
python
# 确保歌曲已取消喜欢
guzhenren_like_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type='Hyperlink')
assert len(guzhenren_like_list) == 0
我的音乐模块测试部分完整代码
暂时就写到这里,最近播放和这两个的思路差不多,大家想写的话最近补充一下:
python
import re
import time
import pyautogui
import pytest
from pywinauto.keyboard import send_keys
class TestMyMusic:
@pytest.mark.order(201)
def test_download(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:下载歌曲 ==========")
# ===== 新增:重置状态 =====
QQMusic_app.logger.info("正在重置界面状态...")
# 点击左上角"推荐"或"音乐"标签,回到主页面
home_tabs = QQMusic_app.win.descendants(title="推荐", control_type="Pane")
if home_tabs:
home_tabs[0].click_input()
time.sleep(2)
# 1. 点击"本地和下载"
QQMusic_app.logger.info("正在进入'本地和下载'...")
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list = [i for i in all_local_list if re.search("本地和下载", i.window_text())]
local_list[0].click_input()
time.sleep(1)
# 点击到下载页面
QQMusic_app.logger.info("正在切换到'下载歌曲'页面...")
download_list = QQMusic_app.win.descendants(control_type='Button')
download = [i for i in download_list if re.search("下载歌曲", i.window_text())]
download_Button = download[0]
download_Button.click_input()
# 确认没有下载蛊真人第一集,如果有删除
QQMusic_app.logger.info("检查是否已存在下载文件...")
time.sleep(1)
one_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)", control_type="Hyperlink")
if len(one_list) > 0:
QQMusic_app.logger.info("发现已下载文件,正在删除...")
one = one_list[0]
one.right_click_input()
for i in range(3):
send_keys("{UP}")
send_keys("{ENTER}")
send_keys("{ENTER}")
time.sleep(2)
QQMusic_app.logger.info("已删除旧文件")
else:
QQMusic_app.logger.info("未发现已下载文件,无需清理")
# 搜索专辑
QQMusic_app.logger.info("正在搜索目标专辑...")
all_edits = QQMusic_app.win.descendants(control_type="Edit")
search_edit = all_edits[0]
search_edit.click_input()
search_edit.type_keys("^a")
search_edit.type_keys("蛊真人|全网更新最快|多人有声{ENTER}")
time.sleep(1)
# 点击"专辑"选项
album_list = QQMusic_app.win.descendants(title="专辑", control_type="Button")
album = album_list[0]
album.click_input()
time.sleep(1)
# 找到目标专辑并点击
all_novel = QQMusic_app.win.descendants(
title="蛊真人|全网更新最快|多人有声剧|大爱仙尊|曲中人工作室|大爱仙尊|古月方源春秋蝉|爆更",
control_type="Hyperlink")
novel = all_novel[0]
novel.click_input()
time.sleep(5)
QQMusic_app.logger.info("已进入专辑详情页")
# 鼠标移动到第一集并下载
QQMusic_app.logger.info("正在定位第一集...")
song_image = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\song_image.png'
x, y = pyautogui.locateCenterOnScreen(song_image)
pyautogui.moveTo(x, y + 80)
QQMusic_app.logger.info("正在点击下载按钮...")
time.sleep(0.5)
song_download1 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download1.png'
song_download2 = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\download2.png'
try:
pyautogui.moveTo(song_download1)
QQMusic_app.logger.info("使用 download1 图标定位成功")
except pyautogui.ImageNotFoundException:
try:
pyautogui.moveTo(song_download2)
QQMusic_app.logger.info("使用 download2 图标定位成功")
except pyautogui.ImageNotFoundException:
QQMusic_app.logger.error("两个下载图标都没找到")
raise Exception("两个图片都没找到")
pyautogui.click()
QQMusic_app.logger.info("正在选择音质...")
time.sleep(1)
music_quality = r'C:\Users\lenovo\Desktop\QQMusicAuto\find_img\music_quality.png'
pyautogui.moveTo(music_quality)
pyautogui.click()
# 回到下载页面验证
QQMusic_app.logger.info("正在返回下载页面验证...")
all_local_list = QQMusic_app.win.descendants(control_type="Pane")
local_list = [i for i in all_local_list if re.search("本地和下载", i.window_text())]
local_list[0].click_input()
time.sleep(1)
download_list = QQMusic_app.win.descendants(control_type='Button')
download = [i for i in download_list if re.search("下载歌曲", i.window_text())]
download_Button = download[0]
download_Button.click_input()
time.sleep(2)
one_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type="Hyperlink")
assert len(one_list) > 0, "下载失败"
QQMusic_app.logger.info("验证通过:歌曲下载成功")
QQMusic_app.logger.info("========== 测试完成:下载歌曲 ==========")
@pytest.mark.order(202)
def test_MyLike(self, QQMusic_app):
QQMusic_app.logger.info("========== 开始测试:我喜欢 ==========")
# 点击"我喜欢"按钮
QQMusic_app.logger.info("正在点击'我喜欢'按钮...")
Mylike_button_list = QQMusic_app.win.descendants(title="我喜欢", control_type="Button")
Mylike_button = Mylike_button_list[0]
Mylike_button.click_input()
# 点击状态栏的"喜欢"
QQMusic_app.logger.info("正在切换到'喜欢'列表...")
all_Mylike_list = QQMusic_app.win.descendants(control_type="Pane")
Mylisk_list = [i for i in all_Mylike_list if re.search("喜欢", i.window_text())]
Mylike = Mylisk_list[0]
Mylike.click_input()
# 搜索喜欢列表里有没有刚添加的歌曲
QQMusic_app.logger.info("正在验证歌曲是否在'我喜欢'列表中...")
guzhenren_like_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type='Hyperlink')
assert len(guzhenren_like_list) > 0
QQMusic_app.logger.info("验证通过:歌曲已在'我喜欢'列表中")
# 取消喜欢,方便下次运行
QQMusic_app.logger.info("正在取消喜欢,还原测试环境...")
guzhenren_like = guzhenren_like_list[0]
guzhenren_like.click_input()
cancel_like_list = QQMusic_app.win.descendants(title="取消喜欢", control_type="Button")
cancel_like = cancel_like_list[0]
cancel_like.click_input()
# 应对弹窗
time.sleep(1)
OK_list = QQMusic_app.win.descendants(title="确定", control_type="Button")
if len(OK_list) > 0:
QQMusic_app.logger.info("检测到确认弹窗,正在点击'确定'...")
OK_list[0].click_input()
else:
QQMusic_app.logger.info("未检测到确认弹窗")
time.sleep(1)
# 确保歌曲已取消喜欢
guzhenren_like_list = QQMusic_app.win.descendants(title="蛊真人 第1集 心仍不悔 (听书搜:曲中人有故事)",
control_type='Hyperlink')
assert len(guzhenren_like_list) == 0
QQMusic_app.logger.info("验证通过:歌曲已成功取消喜欢")
QQMusic_app.logger.info("========== 测试完成:我喜欢 ==========")
生成测试报告
运行文件
这个 run.py 就是一个团队协作时的统一入口 ,让任何人都能用一行 python run.py 跑完所有测试,并且自动记录日志和利用 pytest.ini 中的默认配置。简单、实用、好维护。
python
# run.py - 自动化测试的统一入口脚本
# === 导入模块 ===
import pytest # pytest 测试框架,用于收集和执行测试用例
from Utils.logUtils import QQMusicLogger # 项目中自定义的日志管理类,用于记录测试过程
# === 主程序入口 ===
# 只有直接运行 run.py 时,下面的代码才会执行
# 如果被其他文件 import,则不会执行这部分
if __name__ == "__main__":
# 1. 获取全局唯一的日志记录器对象
# QQMusicLogger 采用单例模式,确保所有地方使用的是同一个日志配置
log = QQMusicLogger.getLogger()
# 2. 记录一条 INFO 级别的日志,表示测试开始
# 这条日志会同时写入总日志文件、info 日志文件,方便后续追溯
log.info("开始运行全部测试用例...")
# 3. 调用 pytest 的 main 函数,运行指定的测试文件
# pytest.main() 等价于在命令行中执行 pytest test_01.py test_02.py test_03.py
# 它会自动读取项目根目录下的 pytest.ini 配置文件(如 addopts、markers 等)
# 返回值 exit_code 是一个整数:
# 0 - 所有测试用例通过
# 1 - 至少有一个测试用例失败
# 2 - 测试被用户中断(如按了 Ctrl+C)
# 其他值 - 其他错误(如 pytest 内部错误、插件加载失败等)
exit_code = pytest.main([
"test_01.py", # 测试用例文件 1:搜索、窗口操作、导入、播放模式等
"test_02.py", # 测试用例文件 2:音量调节、换肤、下载歌曲、我喜欢等
"test_03.py", # 测试用例文件 3:最近播放、推荐页面等
])
# 4. 记录测试结束的日志,并写入退出码
# 通过退出码可以快速判断本次测试的整体结果
log.info(f"测试结束,退出码: {exit_code}")
# 5. (可选)如果需要生成 Allure 测试报告,可以添加以下参数
# "--alluredir=reports/source" # 指定 Allure 报告源数据存放目录
# 然后使用 allure serve reports/source 命令查看报告
pytest.ini 文件加配
|------------------------------------|------------------------------------------------------|
| --alluredir=./reports/source | 指定 Allure 测试报告源数据的存放目录为 ./reports/source(相对于项目根目录) |
| --clean-alluredir | 每次运行前自动清空上一次的报告源数据,避免新旧数据混在一起 |
| testpaths = tests | 指定 pytest 只在 tests 目录下搜索测试用例,而不是扫描整个项目 |
python
[pytest]
addopts = -vs -p no:faulthandler --alluredir=./reports/source --clean-alluredir
testpaths = tests
这个配置文件解决了什么问题?
| 需求 | 解决方案 |
|---|---|
| 每次运行都要手动指定测试目录 | testpaths = tests 让 pytest 默认就去 tests/ 找用例 |
| 生成 Allure 报告 | --alluredir=./reports/source 自动生成报告数据 |
| 旧报告数据干扰新报告 | --clean-alluredir 自动清理,保证每次报告都是最新的 |
| 控制台输出清晰可读 | -vs 保留详细输出和 print 内容 |
| 避免底层 COM 错误刷屏 | -p no:faulthandler 禁用错误堆栈打印 |
allure 生成测试报告
在项目根目录创建文件夹reports,reports下存在source文件夹和html文件夹:

控制台输入:
python
pytest --alluredir=.\reports\source
含义为在source文件夹下生成测试报告,运行后会自动执行所有用例。
之后source会有很多文件生成,我们不用管。

控制台输入:
python
allure generate .\reports\source -o .\reports\html
其含义为根据source文件夹生成html网页,网页为可视化的测试报告效果如图:

可查看每个用例的详细信息:

字数太多了,作者也是写的时候会有不少问题。大家尽量克服,文章写的不好的地方大家见谅。
图片对照表
这是文中用到的图片,大家不知道截取哪一部分可以对照:

学会了就给博主点个赞呗?(✪ω✪)
---------(如有问题,欢迎评论区提问)---------
