QQ音乐自动化测试实战指南

前言

本项目对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、没有 AutomationIdClassName 也为空,ControlType 只是一个通用的 Pane。这意味着它完全无法通过 pywinauto 的标准属性进行定位,因为它根本没有向 UI Automation 框架暴露自己的身份。

这是典型的自绘按钮,我们需要用图像识别法来操作:

图像识别就是让程序"用眼睛看屏幕",而不是"去后台查户口"。

控件识别 vs 图像识别

对比项 控件识别(pywinauto 图像识别(pyautogui
原理 通过 UI Automation 接口,直接读取程序的"控件户口本",拿到按钮的名字、ID、坐标。 截取整个屏幕(或窗口),在像素矩阵里搜索和你提供的图片最相似的一块区域。
依赖 程序必须把控件信息注册到系统中。 只依赖屏幕显示内容,不关心程序内部如何实现。
优点 速度快、稳定、能读取文本内容、不受窗口遮挡或分辨率影响。 无视任何自绘、跨平台框架、游戏界面。只要肉眼能看到,它就能点。
缺点 遇到自绘控件、Web 内嵌页面直接"瞎"。 速度稍慢、依赖屏幕分辨率/DPI/主题颜色、窗口被遮挡时会失败、无法读取文字。

图像识别具体怎么做?

pyautogui 为例,核心就三步:

  1. 提前截图 :把你要点击的按钮单独截一张小图,保存为 button.png(只截按钮,不要截背景)。

  2. 让程序找图 :调用 pyautogui.locateCenterOnScreen('button.png'),它会返回图片在屏幕上出现时的中心坐标 (x, y)

  3. 点击坐标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网页,网页为可视化的测试报告效果如图:

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

字数太多了,作者也是写的时候会有不少问题。大家尽量克服,文章写的不好的地方大家见谅。

图片对照表

这是文中用到的图片,大家不知道截取哪一部分可以对照:


学会了就给博主点个赞呗?(✪ω✪)

---------(如有问题,欢迎评论区提问)---------

相关推荐
m0_716430072 小时前
实现 Flex 容器内子元素自适应高度并启用自动滚动
jvm·数据库·python
weixin_381288182 小时前
c++怎么在写入文本文件时自动将所有的制表符统一转换为四格空格【实战】
jvm·数据库·python
聆风吟º2 小时前
【Python编程日志】Python入门基础(二):行 | 缩进 | print输出
开发语言·python·print··缩进
m0_743623922 小时前
MySQL导入大SQL文件报错怎么办_拆分文件与优化系统参数
jvm·数据库·python
weixin_424999362 小时前
组件懒加载如何处理 JS 报错后的重试加载?保障应用高可用性实战
jvm·数据库·python
baidu_340998822 小时前
SQL中如何通过视图实现行级加密_CASE WHEN语句的妙用
jvm·数据库·python
qq_372154232 小时前
mysql如何限制单用户最大连接数_修改max_user_connections
jvm·数据库·python
卷心菜狗2 小时前
Python进阶-闭包与装饰器
开发语言·python·学习
forEverPlume2 小时前
CSS如何实现背景颜色的棋盘格分布_利用repeating-gradient
jvm·数据库·python