基于 Pywinauto 的 QQ 音乐 GUI 自动化测试实践

一、需求分析与目标制定

在桌面应用迭代过程中,回归测试往往需要重复验证界面交互与核心功能,手动测试不仅效率低下,还容易出现人为疏漏。因此,本次项目以QQ 音乐客户端为测试对象,开展 GUI 自动化测试实践。

本次测试的核心目标是:对 QQ 音乐的核心功能实现自动化回归验证,包括音乐播放控制(播放 / 暂停 / 切歌 / 循环模式)、搜索功能、歌单管理(本地下载 / 我喜欢 / 最近播放)、用户交互(换肤 / 窗口控制)等模块,验证功能稳定性、界面交互响应与元素显示正确性,降低后续版本迭代的回归测试成本。


二、测试用例设计

GUI 自动化测试的核心是围绕界面验证功能验证展开,确保用户交互的合理性与业务逻辑的正确性。结合 QQ 音乐的功能结构,本次测试用例设计如下:


三、测试框架与脚本开发

本次测试采用 Pywinauto + pytest + Allure 技术栈,实现桌面应用的 GUI 自动化测试与可视化报告生成,项目架构清晰,模块解耦,便于后续扩展维护。

1. 安装需要下载的包

在terminal下运行安装

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

2. 核心模块实现

2.1 建立基本框架

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

代码实现:

python 复制代码
import pytest
from pywinauto import Application
from pywinauto.findwindows import ElementNotFoundError
import time
import sys
import os
# 导入日志工具类
from Utils.logUtils import Logger


class QQMusicApp:
    def __init__(self):
        # 全局变量:保存QQ音乐的app实例,方便其他函数调用
        self.app_path = r"C:\Users\Administrator\Desktop\qqmusic\QQMusic.exe"
        self.logger = Logger.getlog()
        self.app = None
        self.win = None

    # ---------- 启动QQ音乐 ----------
    def launch(self):
        try:
            self.app = Application(backend="uia").start(self.app_path)
            # self.app = Application(backend="uia").connect(process=19680)
            time.sleep(3)
            self.win = self.app.window(title="QQMusic")
            self.win.wait("visible")
            self.logger.info("QQmusic应用启动成功")
        except Exception as e:
            self.logger.error(f"QQmusic应用启动失败:{e}")

    # ---------- 关闭QQ音乐(推荐:模拟点击关闭按钮) ----------
    def close(self):
        """正常关闭QQ音乐(模拟点击窗口关闭按钮)"""
        if self.win:
            print("self.win.close()")
        print("✅ 已正常关闭QQ音乐")


@pytest.fixture(scope="session")
def qq_music_app():
    qq_music = QQMusicApp()
    qq_music.launch()
    yield qq_music
    qq_music.close()

配置pytest的默认参数

python 复制代码
[pytest]
# 1. addopts:pytest 运行时 自动带上的命令行参数(不用每次手动敲)
# -v:详细输出,显示每个用例名字和结果
# -s:输出代码里的 print 打印内容
# -p no:faulthandler:关闭 pytest 自带的崩溃捕获(解决Pywinauto/桌面应用卡死问题)
# --alluredir=./reports/source:生成 Allure 原始报告到这个目录
# --clean-alluredir:每次运行前清空旧报告,避免历史数据干扰
addopts = -vs -p no:faulthandler --alluredir=./reports/source --clean-alluredir

# 2. testpaths:告诉pytest,测试用例在哪个文件夹里
# 这里表示:测试用例都放在 ./tests 目录下
testpaths=tests

# 3. python_files:pytest 识别哪些文件是测试文件
# 这里表示:以 test_ 开头的 .py 文件,都会被当作测试文件
; python_files=test_*.py

# 4. python_classes:pytest 识别哪些类是测试类
# 这里表示:以 Test 开头的类,会被当作测试类
; python_classes=Test*

# 5. python_functions:pytest 识别哪些方法是测试用例
# 这里表示:以 test_ 开头的函数/方法,会被当作测试用例
; python_functions=test_*
2.2 日志工具封装(log_util.py)

实现日志分级输出,分别保存 info 和 error 日志,便于问题定位:

python 复制代码
import logging
import os.path
import time


class InfoFilter(logging.Filter):
    def filter(self, record):
        return record.levelno == logging.INFO


class ErrFileter(logging.Filter):
    def filter(self, record):
        return record.levelno == logging.ERROR


class Logger:
    logger = None

    @classmethod
    def getlog(cls):
        # 创建日志对象
        if cls.logger is None:
            cls.logger = logging.getLogger(__name__)
            # 设置日志级别
            cls.logger.setLevel(logging.DEBUG)

            LOG_PATH = "logs/"
            if not os.path.exists(LOG_PATH):
                os.mkdir(LOG_PATH)

            # 2025-06-30.log  2025-06-30_err.log  2025-06-30_info.log
            now = time.strftime("%Y-%m-%d")

            logname = LOG_PATH + now + ".log"
            info_logname = LOG_PATH + now + "_info.log"
            err_logname = LOG_PATH + now + "_err.log"

            # 创建总日志文件处理器
            handler = logging.FileHandler(logname, encoding="utf-8")

            # 创建info日志文件处理器
            info_handler = logging.FileHandler(info_logname, encoding="utf-8")
            info_handler.addFilter(InfoFilter())

            # 创建err日志文件处理器
            err_handler = logging.FileHandler(err_logname, encoding="utf-8")
            err_handler.addFilter(ErrFileter())

            # 设置日志格式
            formatter = logging.Formatter(
                "%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s"
            )

            handler.setFormatter(formatter)
            info_handler.setFormatter(formatter)
            err_handler.setFormatter(formatter)

            # 给logger对象添加handler
            cls.logger.addHandler(handler)
            cls.logger.addHandler(info_handler)
            cls.logger.addHandler(err_handler)
        return cls.logger
2.3 YAML 配置读取工具(yaml_util.py)

将页面元素定位信息存入 YAML 文件,实现用例与元素定位的解耦,便于后续维护:

YAML 文件存入页面元素定位信息,实现用例与元素定位的解耦,便于后续维护

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


def read_yaml(key):
    with open(os.getcwd() + "/data/elements.yml", mode="r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
        return data[key]
2.4 测试用例实现示例(test_common.py)

以公共模块为例,实现核心功能的自动化测试,代码执行时对测试的GUI脚本单独执行:

python 复制代码
pytest ./tests/test_common.py::TestCommon::test_skin
好处 说明
调试效率高 只运行你当前修改 / 调试的用例,不用跑全量测试,节省大量时间
定位问题精准 某个用例失败时,直接单独执行它,快速复现和排查问题
环境干扰少 避免其他用例的执行状态影响当前测试(比如播放状态、窗口位置)
灵活度高 可以只执行某个模块的用例,比如只测换肤、只测本地下载,按需执行
开发效率高 开发新用例时,只运行当前用例,验证功能是否正常,不用等全量执行
python 复制代码
import math
import time

import pytest
from pywinauto import mouse

from Utils.yamlUtils import read_yaml
from Utils.logUtils import Logger


@pytest.mark.order(1)
class TestCommon:
    logger = Logger.getlog()
    '''
    测试logo
    '''
    def test_logo(self, qq_music_app):
        logo_ele = read_yaml("logo")
        logo = qq_music_app.win.child_window(auto_id=logo_ele['auto_id'], control_type=logo_ele["control_type"])
        logo.wait("visible")

    '''
    测试------搜索功能
    '''
    def test_search(self, qq_music_app):
        edit_ele = read_yaml("search")
        edit = qq_music_app.win.child_window(auto_id=edit_ele['auto_id'], control_type=edit_ele["control_type"])
        # 唤起输入框
        edit.click_input()
        # ctrl+a全部选中之后再输入关键词,就不会存在追加的情况
        edit.type_keys("^a邓紫棋")

    '''
    测试------换皮肤
    '''
    def test_skin(self, qq_music_app):
        skin_ele = read_yaml("换肤")
        skin = qq_music_app.win.child_window(auto_id=skin_ele['auto_id'], control_type=skin_ele['control_type'])
        # 点击换肤入口,唤起弹窗
        skin.click_input()

        # 验证弹窗以及文本信息
        warning = qq_music_app.win.child_window(title="温馨提示", control_type="Window")
        warning.wait("visible")

        warn_text = warning.child_window(control_type="Text").window_text()

        assert warn_text == "换肤功能小哥哥正在紧急支持中..."

        # 关闭温馨提示弹窗
        warning.close()
        # 测试弹窗是否正确关闭
        warning.wait_not("visible")

    '''
    测试------最小化
    '''
    def test_window_min(self, qq_music_app):
        window_min_ele = read_yaml("最小化")
        window_min_btn = qq_music_app.win.child_window(auto_id=window_min_ele['auto_id'], control_type=window_min_ele['control_type'])
        # 点击最小化按钮
        window_min_btn.click_input()

        # 测试一下QQ音乐窗口是否已经最小化了
        assert qq_music_app.win.is_minimized()
        # 还原
        qq_music_app.win.restore()

    '''
    测试------导入音乐
    '''
    def test_import_music(self, qq_music_app):
        import_ele = read_yaml("导入音乐")
        import_btn = qq_music_app.win.child_window(auto_id=import_ele['auto_id'], control_type=import_ele['control_type'])
        # 点击导入音乐按钮
        import_btn.click_input()

        # 定位添加本地下载音乐窗口
        import_win = qq_music_app.win.child_window(title="添加本地下载音乐", control_type="Window")
        import_win.wait("visible")

        # 选中所有音乐并添加
        music_list = import_win.child_window(title="项目视图", control_type="List")
        # 打开音乐:1)通过"打开"按钮来实现 2)enter键实现
        music_list.type_keys("^a{ENTER}")

        import_win.wait_not("visible")

    '''
    播放控制模块------随机播放
    默认模式就是随机播放
    默认是暂停
    '''
    def test_play_random(self, qq_music_app):
        # 点击播放全部
        local_ele = read_yaml("本地下载")
        play_all_ele = local_ele["播放全部"]
        play_btn = qq_music_app.win.child_window(auto_id=play_all_ele['auto_id'], control_type=play_all_ele['control_type'])

        for i in range(1, 4):
            # 点击播放全部,从第一首歌曲开始播放(2002年的第一场雪)
            play_btn.click_input()

            # 将歌曲播放进度拉到尾部
            play_ele = read_yaml("播放控制")
            process_line_ele = play_ele["播放总进度"]
            process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])

            # 获取进度条的尺寸
            rec = process_line.rectangle()
            x = rec.right - 3
            y = math.floor((rec.top + rec.bottom)/2)
            # 鼠标点击进度条的尾部
            mouse.click(coords=(x, y))

            # 等待切换下一曲
            time.sleep(2)
            # 检查下一步是否为列表中第二首歌曲
            #             1)若是,随机播放模式不一定错误
            #             2)若不是,随机播放模式正确
            music_name_ele = play_ele["歌曲名"]
            music_name = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'],control_type=music_name_ele['control_type']).window_text()
            if music_name != "Andy阿杜":
                self.logger.info(f"第{i}次判断随机播放下一曲正确")
                return
            else:
                self.logger.info(f"第{i}次判断随机播放下一曲错误")
        # 走到这里还没有返回
        raise Exception("随机播放下一曲三次判断均错误")

    '''
    播放控制模块------单曲循环
    默认模式就是随机播放--切换模式
    上一个用例执行完是播放
    '''
    def test_play_single(self, qq_music_app):
        # 点击播放全部
        local_ele = read_yaml("本地下载")
        play_all_ele = local_ele["播放全部"]
        play_btn = qq_music_app.win.child_window(auto_id=play_all_ele['auto_id'], control_type=play_all_ele['control_type'])
        # 切换模式:随机播放------单曲循环
        play_ele = read_yaml("播放控制")
        playMode_ele = play_ele["模式切换"]
        playMode_btn = qq_music_app.win.child_window(auto_id=playMode_ele['auto_id'], control_type=playMode_ele['control_type'])
        # 点击切换模式按钮
        playMode_btn.click_input()

        for i in range(1, 4):
            # 点击播放全部按钮
            play_btn.click_input()

            music_name_ele = play_ele["歌曲名"]
            music_name_before = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()

            # 将歌曲播放进度拉到尾部
            process_line_ele = play_ele["播放总进度"]
            process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])
            # 获取进度条的尺寸
            rec = process_line.rectangle()
            x = rec.right - 3
            y = math.floor((rec.top + rec.bottom) / 2)

            # 鼠标点击进度条的尾部
            mouse.click(coords=(x, y))

            # 等待切换下一曲
            time.sleep(2)
            # 下一首播放的歌曲和前一首歌曲是否相同
            #        1)相同,单曲循环模式不一定正确---多次验证
            #        2)不相同,单曲循环模式错误
            music_name_after = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()
            if music_name_before != music_name_after:
                self.logger.error(f"单曲循环模式播放下一首歌曲校验错误,before:{music_name_before},after:{music_name_after}")
                break
            else:
                self.logger.info(f"第{i}次校验单曲循环模式播放下一首歌曲正确")
                if i == 3:
                    return
        raise Exception(f"单曲循环模式播放下一首歌曲校验错误,before:{music_name_before},after:{music_name_after}")

    '''
    播放控制模块------列表循环
    默认模式就是单曲循环播放--切换模式
    上一个用例执行完是播放
    '''
    def test_play_circle(self, qq_music_app):
        # 切换模式:单曲循环------列表循环
        play_ele = read_yaml("播放控制")
        music_name_ele = play_ele["歌曲名"]
        play_mode_ele = play_ele["模式切换"]

        # 点击切换模式按钮
        play_mode_btn = qq_music_app.win.child_window(auto_id=play_mode_ele['auto_id'], control_type=play_mode_ele['control_type'])
        play_mode_btn.click_input()

        for i in range(1, 4):
            # 找到列表中最后一首歌曲
            music_list_ele = read_yaml("歌曲列表")
            music_list = qq_music_app.win.child_window(auto_id=music_list_ele['auto_id'], control_type=music_list_ele['control_type'])
            # 获取歌曲列表的中间坐标
            list_mid = music_list.rectangle().mid_point()
            # 鼠标下拉列表使其展示最后一首歌曲
            mouse.scroll(coords=(list_mid.x, list_mid.y), wheel_dist=-500)
            # 获取最后一首歌曲------求列表中列表项目数
            list_size = music_list.item_count()
            # 双击最后一首歌曲,使其播放
            last_music_mid = music_list.get_item(row=list_size-1).rectangle().mid_point()
            mouse.double_click(coords=(last_music_mid.x, last_music_mid.y))

            # 拉取进度条到尾部,等待播放下一曲
            process_line_ele = play_ele["播放总进度"]
            process_line = qq_music_app.win.child_window(auto_id=process_line_ele['auto_id'], control_type=process_line_ele['control_type'])
            # 获取进度条的尺寸
            rec = process_line.rectangle()
            x = rec.right - 3
            y = math.floor((rec.top + rec.bottom) / 2)

            # 鼠标点击进度条的尾部
            mouse.click(coords=(x, y))

            # 等待切换下一曲
            time.sleep(2)
            # 校验播放的下一首歌曲是否为"2002年的第一场雪(列表的第一首歌曲)"
            #            1)是,列表循环校验不一定正确
            #            2)不是,列表循环校验错误
            music_name = qq_music_app.win.child_window(auto_id=music_name_ele['auto_id'], control_type=music_name_ele['control_type']).window_text()
            if music_name != "2002年的第一场雪":
                self.logger.error(f"列表循环下一曲错误,music_name:{music_name}")
                break
            else:
                self.logger.info(f"第{i}次校验列表循环下一曲正确")
                if i == 3:
                    return
        raise Exception(f"列表循环下一曲错误,music_name:{music_name}")
2.5我的喜欢 测试用例实现示例(test_like.py)
python 复制代码
import math
import time

import pytest
from pywinauto import mouse

from Utils.yamlUtils import read_yaml

@pytest.mark.order(4)
class TestLike:
    '''
    测试我喜欢------文本
    "我喜欢、歌曲名称、歌手名称、专辑名称"
    '''
    def test_like_text(self, qq_music_app):
        like_ele = read_yaml("我喜欢")
        # 点击导航栏"我喜欢"进入到我喜欢模块
        like_btn = qq_music_app.win.child_window(auto_id=like_ele["auto_id"], control_type=like_ele["control_type"])
        like_btn.click_input()
        # 测试"我喜欢"文本
        like_text_ele = like_ele["我喜欢文本"]
        like_text = qq_music_app.win.child_window(auto_id=like_text_ele["auto_id"], control_type=like_text_ele["control_type"]).window_text()
        assert like_text == "我喜欢"

        # 测试"歌曲名称"文本
        songname_text_ele = like_ele["歌曲名称文本"]
        songname_text = qq_music_app.win.child_window(auto_id=songname_text_ele["auto_id"], control_type=songname_text_ele["control_type"]).window_text()
        assert songname_text == "歌曲名称"

        # 测试"歌手名称"文本
        singername_text_ele = like_ele["歌手名称文本"]
        singername_text = qq_music_app.win.child_window(auto_id=singername_text_ele["auto_id"], control_type=singername_text_ele["control_type"]).window_text()
        assert singername_text == "歌手名称"

        # 测试"专辑名称"文本
        albumrname_text_ele = like_ele["专辑名称文本"]
        albumrname_text = qq_music_app.win.child_window(auto_id=albumrname_text_ele["auto_id"], control_type=albumrname_text_ele["control_type"]).window_text()
        assert albumrname_text == "专辑名称"

    '''
    我喜欢模块------播放全部
    '''
    def test_like_playAll(self,qq_music_app):
        playAll_ele = read_yaml("我喜欢")["播放全部"]
        playAll_btn = qq_music_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
        # 点击播放全部按钮
        playAll_btn.click_input()
        # 获取播放进度
        process_line_ele = read_yaml("播放控制")["当前播放进度"]
        process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_before = process_line_before.rectangle().right
        # 等待两秒
        time.sleep(2)
        # 获取播放进度
        process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"],
                                                           control_type=process_line_ele["control_type"])
        process_line_len_after = process_line_after.rectangle().right
        # 比较前后两次进度变化,有变化则说明按钮没有问题
        assert process_line_len_before != process_line_len_after

    '''
    我喜欢模块------选择歌曲双击播放
    '''
    def test_like_playSingle(self,qq_music_app):
        music_list_ele = read_yaml("我喜欢")["歌曲列表"]
        music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],control_type=music_list_ele["control_type"])
        # 获取歌曲列表歌曲数量
        list_size = music_list.item_count()
        if list_size <= 0:
            assert 0,"歌曲列表为空"
        # 选择第一首歌曲双击播放
        point = music_list.get_item(row=0).rectangle().mid_point()
        mouse.double_click(coords=(point.x,point.y))
        # 获取播放进度
        process_line_ele = read_yaml("播放控制")["当前播放进度"]
        process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_before = process_line_before.rectangle().right
        # 等待两秒
        time.sleep(2)
        # 获取播放进度
        process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_after = process_line_after.rectangle().right
        # 比较前后两次进度变化,有变化则说明双击歌曲播放没有问题
        assert process_line_len_before != process_line_len_after

    '''
    我喜欢模块------测试标记喜欢
    '''
    def test_mark_unLike(self,qq_music_app):
        # 获取歌曲列表中歌曲的数量
        music_list_ele = read_yaml("我喜欢")["歌曲列表"]
        music_list_before = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],
                                                  control_type=music_list_ele["control_type"])
        # 获取歌曲列表歌曲数量
        list_size_before = music_list_before.item_count()
        # 取消标记喜欢
        rec = music_list_before.get_item(row=0).rectangle()
        y = math.floor((rec.top + rec.bottom)/2)
        x = rec.left + 22
        mouse.click(coords=(x,y))
        # 获取歌曲列表中歌曲的数量
        music_list_after = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"],
                                                         control_type=music_list_ele["control_type"])
        # 获取歌曲列表歌曲数量
        list_size_after = music_list_after.item_count()
        # 测试取消标记喜欢是否成功
        assert list_size_after + 1 == list_size_before
        
2.6本地下载模块 测试用例实现示例
python 复制代码
import math
import time

import pytest
from pywinauto import mouse

from Utils.yamlUtils import read_yaml


@pytest.mark.order(3)
class TestLocal:

    """
    测试本地下载模块------文本
    "本地音乐、歌曲名称、歌手名称、专辑名称"
    """
    def test_local_text(self, qq_music_app):
        local_ele = read_yaml("本地下载")
        # 点击导航栏-本地下载,进入本地下载页面
        local = qq_music_app.win.child_window(auto_id=local_ele["auto_id"], control_type=local_ele["control_type"])
        local.click_input()
        # 测试"本地音乐文本"
        local_text_ele = local_ele["本地音乐文本"]
        local_text = qq_music_app.win.child_window(auto_id=local_text_ele["auto_id"], control_type=local_text_ele["control_type"]).window_text()
        assert local_text == "本地音乐"

        # 测试"歌曲名称文本"
        songname_text_ele = local_ele["歌曲名称文本"]
        songname_text = qq_music_app.win.child_window(auto_id=songname_text_ele["auto_id"], control_type=songname_text_ele["control_type"]).window_text()
        assert songname_text == "歌曲名称"

        # 测试"歌手名称文本"
        singername_text_ele = local_ele["歌手名称文本"]
        singername_text = qq_music_app.win.child_window(auto_id=singername_text_ele["auto_id"], control_type=singername_text_ele["control_type"]).window_text()
        assert singername_text == "歌手名称"

        # 测试"专辑名称文本"
        Albumname_text_ele = local_ele["专辑名称文本"]
        Albumrname_text = qq_music_app.win.child_window(auto_id=Albumname_text_ele["auto_id"], control_type=Albumname_text_ele["control_type"]).window_text()
        assert Albumrname_text == "专辑名称"

    '''
    测试本地下载模块------播放全部功能
    '''
    def test_local_playAll(self, qq_music_app):
        local_ele = read_yaml("本地下载")
        playAll_ele = local_ele["播放全部"]
        playAll_btn = qq_music_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
        # 点击播放全部按钮
        playAll_btn.click_input()
        # 获取播放进度
        process_line_ele = read_yaml("播放控制")["当前播放进度"]
        process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_before = process_line_before.rectangle().right
        # 等待两秒
        time.sleep(2)
        # 获取播放进度
        process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_after = process_line_after.rectangle().right
        # 测试前后两个进度是否存在差别
        assert process_line_len_before != process_line_len_after

    '''
    测试本地下载模块------选择歌曲并双击播放
    '''
    def test_local_playSingle(self, qq_music_app):
        music_list_ele = read_yaml("歌曲列表")
        music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"], control_type=music_list_ele["control_type"])
        # 将歌曲列表还原到最上方------------------公共模块测试循环播放找最后一首歌曲将列表拉到了最下面
        point = music_list.rectangle().mid_point()
        mouse.scroll(coords=(point.x, point.y), wheel_dist=500)
        # 获取歌曲列表中歌曲数量
        if music_list.item_count() <= 0:
            assert 0, "歌曲列表为空"
        # 选择一首歌曲并双击播放
        point = music_list.get_item(row=0).rectangle().mid_point()
        mouse.double_click(coords=(point.x, point.y))
        # 获取播放进度
        process_line_ele = read_yaml("播放控制")["当前播放进度"]
        process_line_before = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_before = process_line_before.rectangle().right
        # 等待两秒
        time.sleep(2)
        # 获取播放进度
        process_line_after = qq_music_app.win.child_window(auto_id=process_line_ele["auto_id"], control_type=process_line_ele["control_type"])
        process_line_len_after = process_line_after.rectangle().right
        # 测试前后两个进度是否存在差别
        assert process_line_len_before != process_line_len_after

    '''
    将歌曲标记喜欢------为了后面我喜欢模块的测试提供数据
    '''
    def test_mark_like(self,qq_music_app):
        # 获取歌曲列表中歌曲数量
        music_list_ele = read_yaml("歌曲列表")
        music_list = qq_music_app.win.child_window(auto_id=music_list_ele["auto_id"], control_type=music_list_ele["control_type"])
        list_size = music_list.item_count()
        # 对每一首歌曲标记喜欢
        for i in range(0, list_size):
            if i != 0 and i % 6 == 0:
                # 6及以后的歌曲在标记喜欢之前需要先向下滑动,使其显示出来
                point = music_list.rectangle().mid_point()
                mouse.scroll(coords=(point.x,point.y),wheel_dist=-500)
            rec = music_list.get_item(row=i).rectangle()
            # 获取爱心的中间位置(x,y)
            y = math.floor((rec.top + rec.bottom)/2)
            x = rec.left + 22
            mouse.click(coords=(x, y))
2.7 推荐模块 测试用例实现示例
python 复制代码
import pytest
from pywinauto import mouse

from Utils.logUtils import Logger
from Utils.yamlUtils import read_yaml


@pytest.mark.order(2)
class TestRecommend:
    logger = Logger.getlog()

    '''
    测试------推荐页面的文本
    "推荐、今日为你推荐、你的音乐补给"
    '''
    def test_rec_text(self, qq_music_app):
        # 点击左侧的推荐导航入口,进入到推荐页面
        rec_ele = read_yaml("推荐")
        rec_btn = qq_music_app.win.child_window(auto_id=rec_ele["auto_id"], control_type=rec_ele["control_type"])
        rec_btn.click_input()
        # 获取"推荐"文本控件
        rec_text_ele = rec_ele["推荐文本"]
        # 获取"今日为你推荐"文本控件
        rec_foru_text_ele = rec_ele["今日为你推荐文本"]
        # 获取"你的音乐补给"文本控件
        rec_supply_text_ele = rec_ele["你的音乐补给文本"]

        # 校验"推荐"文本控件
        rec_text =  qq_music_app.win.child_window(auto_id=rec_text_ele["auto_id"], control_type=rec_text_ele["control_type"])
        assert rec_text.window_text() == "推荐"

        # 校验"今日为你推荐"文本控件
        rec_foru_text = qq_music_app.win.child_window(auto_id=rec_foru_text_ele["auto_id"], control_type=rec_foru_text_ele["control_type"])
        assert rec_foru_text.window_text() == "今日为你推荐"

        # 校验"你的音乐补给"文本控件
        rec_supply_text = qq_music_app.win.child_window(auto_id=rec_supply_text_ele["auto_id"], control_type=rec_supply_text_ele["control_type"])
        assert rec_supply_text.window_text() == "你的音乐补给"

    '''
    测试今日为你推荐滚动区域------左滚动
    '''
    def test_recforu_scroll_left(self, qq_music_app):
        rec_ele = read_yaml("推荐")

        item_text_ele = rec_ele["今日为你推荐第一项文本"]
        item_text_before = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()

        scroll_left_ele = rec_ele["今日为你推荐左滚动"]
        scroll_left = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"], control_type=scroll_left_ele["control_type"])
        # 点击左滚动按钮
        scroll_left.click_input()
        # 获取推荐项的名称,进行前后对比校验
        item_text_after = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()

        assert item_text_before != item_text_after

        '''
        测试今日为你推荐滚动区域------右滚动
        '''

    def test_recforu_scroll_right(self, qq_music_app):
        rec_ele = read_yaml("推荐")

        item_text_ele = rec_ele["今日为你推荐第一项文本"]
        item_text_before = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()

        scroll_right_ele = rec_ele["今日为你推荐右滚动"]
        scroll_right = qq_music_app.win.child_window(auto_id=scroll_right_ele["auto_id"], control_type=scroll_right_ele["control_type"])
        # 点击右滚动按钮
        scroll_right.click_input()
        # 获取推荐项的名称,进行前后对比校验
        item_text_after = qq_music_app.win.child_window(auto_id=item_text_ele["auto_id"], control_type=item_text_ele["control_type"], found_index=0).window_text()

        assert item_text_before != item_text_after

    '''
    测试------你的音乐补给滚动区域------左滚动
    '''
    def test_supply_scroll_left(self, qq_music_app):
        rec_ele = read_yaml("推荐")
        all_rec_area_ele = rec_ele["推荐整个模块"]
        all_rec_area = qq_music_app.win.child_window(auto_id=all_rec_area_ele["auto_id"],
                                     control_type=all_rec_area_ele["control_type"])
        # 找推荐整个模块的中间坐标
        point = all_rec_area.rectangle().mid_point()
        # 在推荐模块鼠标下拉,展示完整的为你推荐区域
        mouse.scroll(coords=(point.x,point.y),wheel_dist=-500)
        # 点击左滚动按钮
        scroll_left_ele = rec_ele["音乐补给左滚动"]
        one_one_ele = rec_ele["音乐补给第一排第一项文本"]
        two_one_ele = rec_ele["音乐补给第二排第一项文本"]

        one_one_text_before = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
                                                       control_type=one_one_ele["control_type"],
                                                    found_index=0).window_text()
        two_one_text_before = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
                                                       control_type=two_one_ele["control_type"],
                                                    found_index=0).window_text()

        scroll_left_btn = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"],
                                     control_type=scroll_left_ele["control_type"])
        scroll_left_btn.click_input()
        #左滚动结果的校验--项目名称是否变化
        one_one_text_after = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
                                                           control_type=one_one_ele["control_type"],
                                                           found_index=0).window_text()
        two_one_text_after = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
                                                           control_type=two_one_ele["control_type"],
                                                           found_index=0).window_text()
        assert one_one_text_after != one_one_text_before
        assert two_one_text_after != two_one_text_before

    '''
    测试------你的音乐补给滚动区域------右滚动
    '''
    def test_supply_scroll_right(self, qq_music_app):
        rec_ele = read_yaml("推荐")
        all_rec_area_ele = rec_ele["推荐整个模块"]
        all_rec_area = qq_music_app.win.child_window(auto_id=all_rec_area_ele["auto_id"],
                                                    control_type=all_rec_area_ele["control_type"])
        # 找推荐整个模块的中间坐标
        point = all_rec_area.rectangle().mid_point()
        # 在推荐模块鼠标下拉,展示完整的为你推荐区域
        mouse.scroll(coords=(point.x, point.y), wheel_dist=-500)
        # 点击右滚动按钮
        scroll_left_ele = rec_ele["音乐补给右滚动"]
        one_one_ele = rec_ele["音乐补给第一排第一项文本"]
        two_one_ele = rec_ele["音乐补给第二排第一项文本"]

        one_one_text_before = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
                                                           control_type=one_one_ele["control_type"],
                                                           found_index=0).window_text()
        two_one_text_before = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
                                                           control_type=two_one_ele["control_type"],
                                                           found_index=0).window_text()

        scroll_right_btn = qq_music_app.win.child_window(auto_id=scroll_left_ele["auto_id"],
                                                       control_type=scroll_left_ele["control_type"])
        scroll_right_btn.click_input()
        # 左滚动结果的校验--项目名称是否变化
        one_one_text_after = qq_music_app.win.child_window(auto_id=one_one_ele["auto_id"],
                                                          control_type=one_one_ele["control_type"],
                                                          found_index=0).window_text()
        two_one_text_after = qq_music_app.win.child_window(auto_id=two_one_ele["auto_id"],
                                                          control_type=two_one_ele["control_type"],
                                                          found_index=0).window_text()
        assert one_one_text_after != one_one_text_before
        assert two_one_text_after != two_one_text_before
2.8 歌词列表模块
python 复制代码
import time

import pytest

from Utils.logUtils import Logger
from Utils.yamlUtils import read_yaml


@pytest.mark.order(5)
class TestSongWords:
    logger = Logger.getlog()

    '''
    测试歌词页面的标题
    "歌手名、歌曲名"
    '''
    def test_titie_text(self,QQMusic_app):
        song_word_page_ele = read_yaml("歌词入口")
        song_word_btn = QQMusic_app.win.child_window(auto_id=song_word_page_ele["auto_id"], control_type=song_word_page_ele["control_type"])

        # 点击页面的歌词入口,进入到歌词页面
        song_word_btn.click_input()
        # 获取歌手名文本
        singer_text_ele = song_word_page_ele["歌手标题文本"]
        singer_text = QQMusic_app.win.child_window(auto_id=singer_text_ele["auto_id"], control_type=singer_text_ele["control_type"]).window_text()
        # 校验歌手名文本
        assert singer_text == "刀郎"
        # 获取歌曲名文本
        song_text_ele = song_word_page_ele["歌曲名标题文本"]
        song_text = QQMusic_app.win.child_window(auto_id=song_text_ele["auto_id"], control_type=song_text_ele["control_type"]).window_text()
        # 校验歌曲名文本
        assert song_text == "2002年的第一场雪"

    '''
    歌词页面------测试歌词
    从头播放歌曲并立即暂停------------才能获取到歌词列表中的歌手名和歌曲名
    '''
    def test_songwords(self, QQMusic_app):
        likepage_ele = read_yaml("我喜欢")
        wordspage_ele = read_yaml("歌词入口")
        #收起歌词页面
        hide_word_page_ele = wordspage_ele["收起歌词"]
        hide_word_page_btn = QQMusic_app.win.child_window(auto_id=hide_word_page_ele["auto_id"], control_type=hide_word_page_ele["control_type"])
        hide_word_page_btn.click_input()

        # 为后面的测试用例做准备------------点击播放歌曲并立即暂停
        playAll_ele = likepage_ele["播放全部"]
        playAll_btn = QQMusic_app.win.child_window(auto_id=playAll_ele["auto_id"], control_type=playAll_ele["control_type"])
        playAll_btn.click_input()
        # 立即暂停播放
        play_ele = read_yaml("播放控制")["播放"]
        play_btn = QQMusic_app.win.child_window(auto_id=play_ele["auto_id"], control_type=play_ele["control_type"])
        play_btn.click_input()

        # 获取当前正在播放的歌手名和歌曲名
        play_control_ele = read_yaml("播放控制")
        singer_name_ele = play_control_ele["歌手名"]
        song_name_ele = play_control_ele["歌曲名"]
        singer_name = QQMusic_app.win.child_window(auto_id=singer_name_ele["auto_id"], control_type=singer_name_ele["control_type"]).window_text()
        song_name = QQMusic_app.win.child_window(auto_id=song_name_ele["auto_id"], control_type=song_name_ele["control_type"]).window_text()

        # songwordsText = f"{song_name} - {singer_name}"

        #进入歌词页面
        song_word_page_ele = read_yaml("歌词入口")
        song_word_btn = QQMusic_app.win.child_window(auto_id=song_word_page_ele["auto_id"], control_type=song_word_page_ele["control_type"])

        # 点击页面的歌词入口,进入到歌词页面
        song_word_btn.click_input()
        # 测试歌词
        words_list_ele = wordspage_ele["歌词列表"]
        words_list = QQMusic_app.win.child_window(auto_id=words_list_ele["auto_id"], control_type=words_list_ele["control_type"])
        for i in words_list.children():
            if i.window_text() in song_name or i.window_text() in singer_name:
                return
            self.logger.info(f"获取到的歌词:{i.window_text()}")
        # 始终没有匹配上
        raise Exception(f"歌词匹配失败,song_name:{song_name},singer_name{singer_name}")

四、测试执行与报告分析

1. 执行测试用例

通过 pytest 命令批量执行所有测试用例,同时生成 Allure 结果文件:

bash 复制代码
pytest --alluredir .\reports\source\
python 复制代码
allure generate .\reports\source\ -o .\reports\html

执行结果如下:

复制代码
collected 13 items

tests/test_common.py::TestCommon::test_logo PASSED
tests/test_common.py::TestCommon::test_search PASSED
tests/test_common.py::TestCommon::test_skin PASSED
tests/test_common.py::TestCommon::test_importmusic PASSED
tests/test_local.py::TestLocal::test_playAll PASSED
tests/test_local.py::TestLocal::test_music_list PASSED
tests/test_local.py::TestLocal::test_mark_like PASSED
tests/test_recommend.py::TestRecommend::test_rec_foru PASSED
tests/test_recommend.py::TestRecommend::test_supply_Music PASSED
tests/test_like.py::TestLike::test_playAll PASSED
tests/test_like.py::TestLike::test_music_list PASSED
tests/test_like.py::TestLike::test_unmark_like PASSED
tests/test_songwords.py::TestSongWords::test_song_words PASSED

passed in 33.16s

2. 生成 Allure 测试报告

使用 Allure 命令生成可视化静态报告:

python 复制代码
allure generate .\reports\source\ -o .\reports\html

执行后提示:(venv) D:\GUIautotest\QQmusicTest>allure generate .\reports\source\ -o .\reports\html
Report successfully generated to .\reports\html

即可打开 html/index.html 查看报告。

3. 测试结果分析

  • 测试用例总数:共执行 13 个测试用例,覆盖了 QQ 音乐的核心模块与关键交互场景,测试覆盖度较为全面。

  • 通过率:13 个用例全部 PASSED,通过率 100%,说明本次测试的核心功能在当前版本下运行稳定,无明显交互异常。

  • 测试时间:总执行耗时 32 秒 88 毫秒,平均每个用例耗时约 2.5 秒,后续可通过并行执行、优化等待时间进一步缩短测试周期。

  • 问题与优化方向

    1. 部分元素定位依赖 auto_id,若 QQ 音乐版本更新导致控件 id 变化,需重新维护 elements.yml 配置;
    2. 测试用例之间存在一定耦合,如播放模式测试依赖当前播放状态,后续可优化用例隔离性;
    3. 可增加异常场景测试,如断网状态下的在线音乐加载、无权限时的本地音乐导入等。

五、总结与展望

本次基于 Pywinauto 的 QQ 音乐 GUI 自动化测试实践,实现了桌面应用核心功能的自动化回归验证,有效解决了手动测试效率低、重复劳动多的问题。通过 pytest+Allure 的组合,不仅实现了用例的批量执行,还生成了可视化的测试报告,便于结果分析与问题定位。

后续可从以下方向优化扩展:

  1. 引入 pytest-xdist 实现测试用例并行执行,大幅缩短测试时间;
  2. 增加失败用例的截图保存功能,便于问题复现;
  3. 结合 Jenkins 实现自动化测试的定时执行,接入持续集成流程;
  4. 扩展更多异常场景与边界用例,提升测试覆盖度。
相关推荐
人道领域1 小时前
【LeetCode刷题日记】669.修剪二叉搜索树
开发语言·python·算法
EntyIU2 小时前
mineru从安装部署到测试使用完整指南
python·ocr
安替-AnTi3 小时前
厚朴 APK 搜索接口分析
python·apk·解析·taobao
山川湖海3 小时前
AI时代快速学编程语言的陷阱(以Python为例)
大数据·人工智能·python
H Journey3 小时前
Supervisor 进程管理工具介绍
python·supervisor·linux 运维
春日见4 小时前
5分钟入门强化学习之动态规划算法与实现
大数据·人工智能·python·算法·机器学习·计算机视觉
DeniuHe4 小时前
sklearn 中所有交叉验证数据集划分方式完整总结
人工智能·python·sklearn
DeniuHe4 小时前
sklearn中不同交叉验证方法的场景适配
人工智能·python·sklearn
隐于花海,等待花开5 小时前
16.Python 常用第三方库概览 深度解析
python