1. 自动化测试是什么

自动化测试是指利用脚本和工具代替人工重复执行测试步骤、自动验证结果并生成报告的过程。

它的核心目标不是”完全取代人工测试”,而是:

  • 把高频、重复、稳定的验证动作从人工中解放出来。
  • 在每次代码变更后尽快发现问题,而不是等到发版前。
  • 降低回归验证成本——同样的用例可以无限次免费重跑。
  • 固化测试环境、测试数据和测试报告,消除”我这里没问题”。
  • 为发版提供可追溯的客观证据,而不依赖人的记忆和判断。

换句话说:自动化测试解决的是人工测试无法规模化的问题,不是要证明软件没有 bug。

什么情况适合自动化

适合自动化不适合自动化(或收益低)
高频执行、每次发版都要验证只跑一次、需求还在快速变化
步骤固定、结果有明确判断标准需要主观判断(美观、易用性)
回归验证、历史 bug 防复现探索性测试、边界挖掘
数值计算正确性首次验证新功能是否符合预期
多模块集成后的接口联通性极少执行、维护成本高于节省成本

自动化测试分层(测试金字塔)

不同层级的测试,速度、范围、维护成本差异很大。合理的自动化体系是”下多上少”——越底层的测试越多,越顶层的越精简:

              ┌────────────┐
              │  系统测试   │  最慢、最脆、覆盖端到端流程
            ┌─┴────────────┴─┐
            │    回归测试     │  固定数据 + 固定期望,防止历史问题复现
          ┌─┴────────────────┴─┐
          │     集成测试        │  模块间接口、API、数据库
        ┌─┴────────────────────┴─┐
        │       单元测试          │  最快、最稳、覆盖函数/算法局部逻辑
      ┌─┴────────────────────────┴─┐
      │     静态检查 / 构建验证     │  编译通过、依赖完整、配置有效
      └────────────────────────────┘

各层在我们项目中的对应:

层级关注点工具在我们项目中
静态检查 / 构建编译成功、依赖完整CMake、PowerShell模块编译 Job
单元测试函数、类、算法局部逻辑GTest (C++)unit tests
集成测试模块间接口、HTTP API、WebSocket、数据库pytest、requestsintegration tests
回归测试固定数据、数值结果稳定性pytest、golden dataregression tests
系统测试真实 UI、端到端流程、截图对比pywinauto、Windows UIAsystem tests
构建打包验证从源码到安装包的完整可交付链路CMake、NSIS、JenkinsPackage Job
报告归档结果可视化、历史趋势、失败定位JUnit XML、Allure、Jenkins artifact贯穿所有阶段

2、自动化流程包含哪些

自动化测试不只是"写几个脚本",它是一套从代码提交到报告归档的完整流水线。每个环节都有明确职责,缺少任何一环都会导致自动化流于形式。

完整流程

① 触发
    代码提交 / 定时任务 / 手动触发
      ↓
② 拉取代码
    checkout 指定分支或 tag
      ↓
③ 环境验证
    检查编译工具、依赖路径、测试数据、服务可用性
    (提前发现环境问题,不让后续步骤白跑)
      ↓
④ 编译构建
    CMake configure → build → output
      ↓
⑤ 执行测试
    单元测试 → 集成测试 → 回归测试 → 系统测试
    (按层级顺序,前一层失败可选择是否继续)
      ↓
⑥ 生成报告
    JUnit XML → Allure report → Jenkins artifact
      ↓
⑦ 结果处理
    成功:归档报告、触发下游(打包、发版)
    失败:保留现场(截图、日志、diff)、发送通知

每个环节为什么重要

环节如果省略会怎样
环境验证跑到一半才发现 服务没起,浪费 30 分钟
按层级顺序执行单元测试没过就跑 UI 测试,结果一堆假报错
失败现场保留测试失败但不知道为什么,只能手动复现
报告归档知道失败了,但找不到上次成功是什么时候
触发条件分离每次提交都跑全量,CI 永远排队

3、主流自动化测试方案

市面上的自动化测试平台非常多,但适合软件、多仓库、Windows GUI 场景的选择其实很有限。以下列出几类主流方案,并说明各自的定位和局限。

3.1 GitLab CI

GitLab CI 是与代码仓库深度集成的 CI 平台,配置写在仓库根目录的 .gitlab-ci.yml 文件中,每次 push 或 MR 自动触发。

git push / merge request
  -> GitLab Runner 拉代码
  -> 按 .gitlab-ci.yml 定义逐 stage 执行
  -> 结果直接显示在 GitLab MR 页面

适合: 每次提交后的快速反馈、单仓库单元测试、轻量构建验证、MR 合并门禁。

局限:

  • 多仓库联动需要跨项目 trigger,配置复杂。
  • Windows GUI 自动化支持差(Runner 通常跑在 Linux 容器中)。
  • 大型本地依赖(Qt、CUDA)、真实桌面会话管理成本高。
  • 无法方便地调度"手动触发、指定节点、跑完归档安装包"这类 Release 流程。

3.2 GitHub Actions

与 GitHub 绑定的 CI/CD 平台,配置写在 .github/workflows/ 中,对开源项目非常友好。

适合: 开源项目、跨平台矩阵测试(Linux/macOS/Windows)、轻量自动发布。

局限:

  • 私有项目有运行分钟数限制(付费才能无限)。
  • 我们使用 GitLab 而非 GitHub,不适用。
  • 同样不适合 Windows GUI 自动化和本地大型依赖。

3.3 Azure DevOps Pipelines

微软生态的 CI/CD 平台,对 Windows、MSVC、MSBuild 原生支持好。

适合: Windows 重度依赖项目、.NET 生态、需要 Azure 云资源的场景。

局限:

  • 需要 Azure 账号,私有部署复杂。
  • 与 GitLab 代码仓库的集成不如 GitLab CI 原生。
  • 对项目而言引入了不必要的外部云依赖。

3.4 Jenkins

Jenkins 是通用自动化调度平台,不依赖特定代码仓库,节点和工作目录完全自定义。

GitLab webhook / 定时任务 / 手动触发
  -> Jenkins Job(指定节点、指定参数)
  -> 执行 Jenkinsfile(Groovy Pipeline)
  -> 调用 PowerShell 脚本:编译、测试、打包
  -> 归档 Allure 报告、JUnit XML、安装包 artifact

适合: 多仓库组合构建、Windows 节点 / GPU 节点 / GUI 节点混合调度、Nightly 定时测试、手动 Release 流水线、安装包打包与归档。

局限:

  • Job 参数、workspace、凭据、节点环境需要统一规范,否则容易"在我机器上好的"。
  • Jenkins 服务账户环境变量与本机桌面用户不完全一致,PATH 可能不同。
  • 多 Job 串联时版本追踪(哪个 Job 的产物给哪个 Job 用)需要额外设计。

3.5 纯脚本方案(PowerShell / Bash / Python)

不依赖任何 CI 平台,直接写脚本手动跑或排 Windows 计划任务。

适合: 团队规模小、CI 平台还没搭建、快速验证某个流程。

局限:

  • 没有集中的历史记录和趋势视图。
  • 失败不会自动通知,依赖人主动去看。
  • 难以管理多机器、多环境的一致性。
  • 规模扩大后维护成本急剧上升。

3.6 主流方案横向对比

维度GitLab CIGitHub ActionsAzure DevOpsJenkins纯脚本
与 GitLab 集成✅ 原生⚠️ 需配置⚠️ webhook
Windows GUI 测试⚠️ 有限⚠️ 有限
多仓库联动⚠️ 复杂⚠️ 复杂⚠️ 复杂
本地大型依赖⚠️⚠️⚠️
手动 Release 流程⚠️⚠️
报告 / 历史趋势⚠️ 基础⚠️ 基础✅ Allure
私有部署❌ 受限❌ 云服务

3.7 为什么选择 GitLab CI + Jenkins 组合

单独用任何一个都有缺口,组合使用才能覆盖我们的需求:

职责平台原因
每次 push 快速编译验证GitLab CI与代码仓库原生绑定,结果直接显示在 MR
Nightly 全量测试Jenkins多仓库、多节点、定时调度,Allure 报告归档
Release 发版门禁Jenkins手动触发、参数化控制、合并 + tag + 触发打包
Windows GUI 系统测试Jenkins(windows-gui 节点)需要真实桌面会话,GitLab CI 做不到
安装包打包归档Jenkins从 tag 独立 checkout,NSIS 打包,artifact 归档

核心原则:GitLab CI 负责"快而轻"的代码门禁,Jenkins 负责"重而全"的测试与发版。


3.8 UI 自动化测试工具对比

CI/CD 平台解决的是"在哪里跑、什么时候跑",而 UI 自动化工具解决的是"怎么操作界面、怎么验证结果"。两者是不同层面的选择,需要分开来看。

Web 应用 UI 自动化
工具定位优点局限
Selenium最老牌的 Web 自动化框架,驱动真实浏览器社区庞大、支持所有主流浏览器和语言速度慢、配置繁琐、浏览器版本兼容问题多
Playwright微软出品,现代 Web 自动化首选速度快、自动等待、跨浏览器(Chrome/Firefox/Safari)、支持 Python/JS/TS相对较新,社区比 Selenium 小
Cypress前端开发友好,运行在浏览器内部实时预览、调试体验好、截图/视频录制内置只支持 Chrome 系、不支持多标签页、不适合后端联调场景
PuppeteerGoogle 出品,专门控制 Chromium轻量、适合爬虫和截图只支持 Chrome,API 偏底层

 React Web 应用,如果要做 Web 端 UI 自动化,Playwright 是当前最推荐的选择:Python 原生支持、自动等待机制好、和现有 pytest 体系可以无缝集成。


桌面客户端 UI 自动化

桌面 GUI 自动化比 Web 复杂得多,不同框架写的 UI(Qt、WPF、WinForms、Win32)底层技术不同,工具选型要匹配。

工具定位优点局限
Squish(Froglogic)Qt 专用商业 UI 测试工具对 Qt 对象识别最精准,支持 QML/Widgets,脚本语言多商业授权费用高,学习曲线陡
pywinautoPython 封装 Windows UIA / MSAA,开源免费、与 pytest 完美集成、适合任何 Windows 程序依赖可访问性树,Qt 控件需要正确暴露 UIA 属性
WinAppDriver微软出品,基于 WebDriver 协议控制 Windows 应用协议标准化、可配合 Appium 使用配置步骤多,微软已停止主动维护
TestCompleteSmartBear 商业全功能 UI 测试平台支持 Web + 桌面 + 移动,录制回放价格高、体量重
Ranorex商业桌面 UI 测试工具录制回放、报告完整商业授权、生成的脚本可读性差
AutoItWindows 脚本语言,模拟键鼠操作轻量、上手快不基于控件识别,依赖坐标/图像,极易因分辨率变化而失效
SikuliX基于图像识别控制 UI不需要源码访问,任何界面都能用对分辨率/DPI/缩放比例极度敏感,稳定性差
为什么我们选择 pywinauto

我们的桌面客户端 Qt6 Widgets 应用,运行在 Windows 上。工具选型考量:

考量点Squishpywinauto(我们的选择)
成本商业授权,价格较高完全免费开源
Qt 控件识别原生 Qt 对象树,最精准依赖 Qt 暴露 UIA 可访问性属性(Qt6 支持较好)
与 pytest 集成需要桥接原生 Python,直接 import
报告(Allure)需要额外开发直接用 allure-pytest,无缝集成
维护学习成本专有 IDE,培训成本高Python 脚本,团队已掌握
CI 集成需要 Squish Server license直接在 Jenkins Windows 节点上运行

结论: Squish 的控件识别精度更高,但对于我们的规模和预算,pywinauto + Windows UIA 的组合已经足够,且与现有 Python/pytest/Allure 技术栈完全统一,维护成本低。

如果未来 Qt 控件可访问性属性暴露不完整导致 pywinauto 定位困难,可以评估引入 Squish 或迁移到 WinAppDriver。

4、什么是 CI/CD 与 Pipeline

4.1 CI/CD 概念

CI/CD 不是某个具体工具,而是一种软件交付实践。它描述的是从代码提交到软件可交付这条链路的自动化程度。

CI — Continuous Integration(持续集成)

关注”代码合进去之前或刚合进去之后是否可靠”。

开发者提交代码
  -> 自动触发构建
  -> 自动运行测试
  -> 结果反馈给开发者
  -> 不通过则阻止合并

目标:尽早发现问题,避免坏代码长期留在主干。问题发现越早,修复成本越低。

CD — Continuous Delivery / Deployment(持续交付 / 持续部署)

关注”软件是否随时处于可交付状态”。

名称含义发布方式
Continuous Delivery(持续交付)每次构建都产出可交付的包,但是否发布由人工决定手动触发发版
Continuous Deployment(持续部署)测试通过后自动推送到生产环境,无需人工确认全自动

医疗软件通常选择 Continuous Delivery:自动构建和测试,但发版由人工审核后触发,保证可控性。我们的方案也是如此。

我们当前的位置:

CI(每次 push 自动编译 + 单元测试)
  + Nightly 自动全量测试
  + 手动触发 Release(测试通过后合并 + 打 tag)
  + 自动打包归档
= 持续交付(Continuous Delivery),而非完全自动部署

4.2 什么是 Pipeline(流水线)

Pipeline 是 CI/CD 的执行单元——把一系列自动化步骤串联成一条有序的流程,每一步称为一个 Stage(阶段)Step(步骤)

Pipeline
  ├── Stage 1: 初始化 / 环境检查
  ├── Stage 2: 编译构建
  ├── Stage 3: 单元测试
  ├── Stage 4: 集成测试
  ├── Stage 5: 报告生成
  └── Stage 6: 归档 / 发版

前一个 Stage 失败,后续 Stage 默认不执行(也可配置为继续)。这样能在最早的阶段停下来,不浪费资源。


4.3 GitLab CI Pipeline 与 Jenkins Pipeline 的差异

两者都叫 Pipeline,但实现方式和适用场景差别较大:

维度GitLab CI PipelineJenkins Pipeline
配置文件.gitlab-ci.yml,存在仓库中Jenkinsfile(Groovy),存在仓库或 Jenkins 里
触发方式push / MR / tag / 定时,自动触发webhook / 定时 / 手动,灵活配置
执行单元Job(跑在 Runner 容器或 shell 中)Stage + Step(跑在 Jenkins agent 节点上)
可视化GitLab 内置 Pipeline 图,与 MR 联动Blue Ocean / 经典视图,需要插件
参数化支持,但较简单支持复杂参数(布尔、下拉、文本、文件)
多节点调度通过 Runner tag 选择节点通过 agent { label '...' } 指定节点
产物归档artifacts 字段,保留在 GitLabarchiveArtifacts,保留在 Jenkins
报告集成基础 JUnit 支持插件丰富(Allure、HTML、Coverage 等)

GitLab CI Pipeline 示例(.gitlab-ci.yml):

stages:
  - build
  - test

build:mevCore:
  stage: build
  script:
    - cmake -B build -S code -DCMAKE_BUILD_TYPE=Release
    - cmake --build build --config Release
  tags:
    - windows

test:unit:
  stage: test
  script:
    - .\build\Release\test.exe --gtest_output=xml:result.xml
  artifacts:
    reports:
      junit: result.xml
  tags:
    - windows

Jenkins Pipeline 示例(Jenkinsfile):

pipeline {
    agent { label 'windows-build' }

    parameters {
        booleanParam(name: 'RUN_UNIT_TESTS', defaultValue: true)
        booleanParam(name: 'UPDATE_QT_UI_BASELINE', defaultValue: false)
    }

    stages {
        stage('Init') {
            steps {
                powershell '.\\scripts\\_01_init.ps1'
            }
        }
        stage('Unit Tests') {
            when { expression { params.RUN_UNIT_TESTS } }
            steps {
                powershell '.\\scripts\\_02_run_unit_tests.ps1'
            }
        }
        stage('Qt UI Tests') {
            agent { label 'windows-gui' }
            steps {
                powershell '''
                    $args = @("-Auto")
                    if ($env:UPDATE_QT_UI_BASELINE -eq "true") {
                        $args += @("-ApproveBaseline", "-AllowBaselineUpdate")
                    }
                    & ".\\scripts\\_06_run_system_tests.ps1" @args
                '''
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'reports/**', allowEmptyArchive: true
            junit 'reports/**/*.xml'
        }
    }
}

Jenkins Pipeline 的几个关键能力:

  • parameters:在 Jenkins UI 上暴露参数,手动触发时可以选择开关(是否更新 baseline、是否跑 UI 测试)。
  • when:条件执行,某个 Stage 只在特定参数或分支下才跑。
  • agent { label '...' }:不同 Stage 可以在不同节点上跑(编译节点 vs GUI 节点)。
  • post { always {} }:无论成功失败都执行,确保报告和日志被归档。
  • 报告汇总:当前 Nightly 在 post { always {} } 中汇总并归档 Allure results、JUnit、日志和截图,保证失败时也有报告。

4.4 我们的 Pipeline 在哪里定义

JobPipeline 文件位置
GitLab push 编译验证.gitlab-ci.yml各模块仓库根目录
Nightly 自动测试Jenkinsfilexxx
Release 发版门禁JenkinsfileReleasexxx
Package 打包JenkinsfilePackagexxx

Jenkinsfile 负责节点、参数、阶段和归档;PowerShell 脚本作为 Windows/本地调用薄入口,负责参数与环境加载;主要测试逻辑位于 Python runner、pytest 用例和 tests/ui/ 动作库中。这样本地仍可直接运行同一组脚本调试,不需要启动 Jenkins。

5、我们的总体设计

5.1 总体目标

我们的自动化设计目标:

  • 每次 push 后尽快知道模块是否能编译。
  • 每晚自动跑 develop 的集成、回归、系统测试。
  • Release 必须人工触发,测试全绿后才合并 main、打 tag、触发打包。
  • 打包必须从明确 tag 独立 checkout,不复用 develop 工作区。
  • 报告、日志、截图、安装包都能在 Jenkins 中查看或归档。

5.2 总体架构

开发提交 develop
  │
  ├─ GitLab CI
  │    └─ 每次 push 自动单元测试 / 基础检查
  │
  ├─ Jenkins 模块编译 Job
  │    ├─ xxx
  │    └─ ...
  │
  ├─ Jenkins Nightly
  │    └─ 每晚自动测试 develop,不合并、不打包
  │
  └─ Jenkins Release
       ├─ 手动触发
       ├─ 可选单元 / 集成 / 回归 / Qt UI 测试
       ├─ 测试通过后 develop -> main
       ├─ 创建 release tag
       └─ 触发 Package

Package
  ├─ 从 tag 独立 checkout
  ├─ 重新编译依赖仓库
  ├─ NSIS 打包
  └─ 归档安装包

5.3 Job 设计

Job触发方式分支/版本目的
xxx等模块 JobGitLab push webhookdevelop编译验证,快速反馈
xxx-Nightly每晚定时develop自动测试,出报告,不动 main
xx-Release手动触发develop -> main发版前自动测试,成功后合并、tag
xxx-FromTagRelease 触发或手动tag从正式 tag 重新编译并打包

6、各类测试详解

6.1 单元测试(Unit Test)

含义

测试最小可测单元——单个函数、类或算法模块,完全不依赖外部服务(不启动数据库、不联网、不打开 UI)。

类比:测试一个零件能不能正常工作,而不是把整台机器装起来试。

特点:速度最快(毫秒级)、最稳定、最容易定位问题。应在每次提交后自动运行。

什么样的测试才能被 CI 自动判定?

CI 能判定通过/失败的前提是:测试有明确的断言,返回码能告诉 ctest 是否通过。项目中存在新

真实示例一:数据模型深拷贝(xxx/tests/test_data_model.cpp

不依赖任何外部文件或服务,纯内存操作,是最理想的单元测试:

TEST(BeamClone, BasicFieldsArePreserved) {
    auto original = xxx::New();
    original->set_xxx_name("Testxxx);

    auto cloned = original->clone();

    EXPECT_EQ(cloned->get_xxx(), "Testxxx");
    EXPECT_DOUBLE_EQ(cloned->get_xxx(), 100.0);
    EXPECT_EQ(cloned->get_xxx(), "xx");
}

TEST(BeamClone, ModifyCloneDoesNotAffectOriginal) {
    auto original = xxx::New();
    original->set_xxx("Testxx");

    auto cloned = original->clone();
    cloned->set_xxx("Modified");

    EXPECT_EQ(original->get_xxxe(), "Testxxx");  // 原对象不受影响
}

真实示例二:解析(xxx/tests/test_xxx.cpp

依赖测试数据文件,用 Fixture + GTEST_SKIP() 优雅处理数据缺失:

class xxxTest : public ::testing::Test {
public:
    static void SetUpTestSuite() {
        const char* root = std::getenv("TPS_TEST_DATA_DIR");
        if (!root) return;          // 环境变量未设置,SetUpTestSuite 空跑
        // 扫描目录,找到xxxx文件
        for (const auto& entry : fs::directory_iterator(root)) {
            std::string xxx;
            xxx dp;
            if (dp.xxx(entry.path().string(), xx) && xx== "xx")
                s_xx_file = entry.path().string();
        }
    }
protected:
    static std::string s_xxx;
};

TEST_F(xxxTest, xxxNonEmpty) {
    if (s_xxx.empty()) GTEST_SKIP() << "No xxx";  // 无数据跳过不报错

    xxx xxx;
    xxx xxx;
    std::vector<xxx> xxxx;

    ASSERT_TRUE(xxx.xxx(s_xxx, xxx, xxx));  // 失败则终止
    EXPECT_FALSE(xxx->get_xxx.empty());                 // xxx 列表非空
    for (const auto& xxx: xxx->get_xxx())
        EXPECT_FALSE(xxx->get_xxx().empty());              // 每个 xxx有名字
}

关键设计:

  • TEST_DATA_DIR 从环境变量读,CI 和本地统一配置,不写死路径。
  • GTEST_SKIP():没有数据时跳过而不报错,编译验证环境不需要测试数据。
  • ASSERT_* 失败立即终止,防止后续访问空指针崩溃;EXPECT_* 失败后继续,尽量多报出问题。

真实示例三:xxx (xxx/tests/test_xxx.cpp

class xxx: public ::testing::Test {
protected:
    void TearDown() override {
        xxx::instance().xxx("test");  // 每个用例后清理,防止互相干扰
    }
};

TEST_F(xxx, xxx) {
    int call_count = 0;
    xxx::instance().xxx(
        xxx::xxx, "test",
        [&](const xxx&) { ++call_count; });

    xxx::instance().xxx(xxx::xxx, {});

    EXPECT_EQ(call_count, 1);  // 回调恰好触发一次
}

TEST_F(xxx, xxx) {
    int call_count = 0;
    xxx::instance().xxx(xxx::xxx, "test",
        [&](const xxx&) { ++call_count; });
    xxx::instance().xxx("test");
    xxx::instance().xxx(xxx::xxx, {});

    EXPECT_EQ(call_count, 0);  // 取消xxx不应触发
}

6.2 集成测试(Integration Test)

含义

测试多个模块组合在一起是否能正确协作,通常需要真实的服务、数据库或网络接口在运行。

类比:零件单独测过了,现在把它们装在一起,看接缝处有没有问题。

特点:比单元测试慢,需要环境准备(启动 服务、数据库),但能发现接口不匹配、配置缺失、数据格式错误等问题。

示例:xxxx导入后可查询(pytest + requests)

# 结构示例;实际用例入口为 testcases/xxx/test_xxx.py
import os
import allure
import requests
import pytest

@pytest.fixture(autouse=True)
def _allure_suite():
    allure.dynamic.label("xxxSuite", "04 集成测试")
    allure.dynamic.label("suite", "02 xxx管理")

def test_import_xxx_appears_in_list(base_url, tmp_xxx):
    """导入 xxx后,xxx列表中应能查到该xxx。"""
    data_dir = os.environ["TEST_DATA_DIR"]
    xxx_path = os.path.join(data_dir, "xxx", "xxx_001")

    with allure.step("提交 xxx导入请求"):
        resp = requests.post(
            f"{base_url}/api/xxx/import",
            json={"path": dxxx_path},
            timeout=30,
        )
        assert resp.status_code == 200, resp.text

    with allure.step("查询xxx列表"):
        patients = requests.get(f"{base_url}/api/xxx", timeout=10).json()

    with allure.step("断言xxx存在"):
        names = [p["xxxx"] for p in xxxx]
        assert "xxx" in names, f"未找到导入的xxx,当前列表:{names}"

设计要点:

  • 前置条件(服务是否就绪)由 conftest.py 的 session fixture 统一检查,不在每个用例里重复。
  • 每个 allure.step 对应一个业务动作,失败时能精确定位是哪一步出了问题。
  • base_url 从 pytest 命令行参数读取,本地和 CI 环境统一。

6.3 回归测试(Regression Test)

含义

固定输入 + 固定期望结果验证历史正确行为没有被破坏,防止"改了 A 之后 B 悄悄坏了"。

类比:工厂每批次生产后都用同一份标准样品对比,确认出厂规格没有偏移。

特点:依赖"golden 数据"(上次公认正确的计算结果),数值测试必须给容差,不能做字符串完全匹配。

示例:xxxx 数值回归(pytest)

# 结构示例;实际回归入口为 testcases/regression/test_regression.py
import json
import math
import allure
import pytest

GOLDEN_FILE = "testdata/golden/xxx.json"
TOLERANCE   = 0.001   # xxx容差 0.1%

@pytest.fixture(autouse=True)
def _allure_suite():
    allure.dynamic.label("parentSuite", "05 回归测试")
    allure.dynamic.label("suite", "数值回归")

def test_xxx_stable(calc_client):
    """xxxx计算通与 golden 结果偏差不超过 0.1%。"""
    with allure.step("执行 xxxx 计算"):
        result = calc_client.run_xxx("xxx")

    with allure.step("加载 golden 数据"):
        with open(GOLDEN_FILE) as f:
            golden = json.load(f)

    with allure.step("对比通过率"):
        diff = abs(result["xxxx"] - golden["xxxx"])
        allure.attach(
            f'actual={result["xxx"]:.4f}  golden={golden["xxx"]:.4f}  diff={diff:.4f}',
            name="xxx对比", attachment_type=allure.attachment_type.TEXT
        )
        assert math.isclose(result["xxx"], golden["xxx"], abs_tol=TOLERANCE), \
            f"xxx偏移 {diff:.4f} 超过容差 {TOLERANCE}"

设计要点:

  • Golden 文件随代码一起提交到 Git,保证每个人、每台机器用相同基准。
  • abs_tol 而非 rel_tol:xxx等物理量用绝对容差更直观。
  • 失败时把 actual/golden/diff 一起附到 Allure,不需要重跑就能看出偏移量。
  • xxx中通过 ctest -L regression 标签区分:普通 push 跑快速用例,定时调度才跑回归。

6.4 系统测试(System Test)

含义

在真实程序、真实 UI、真实环境中跑完整用户流程,最接近人工验收测试。

类比:把整台机器交给用户之前,模拟用户操作一遍,看会不会出问题。

特点:最慢、最容易受环境影响(分辨率、GPU、RDP 会话),但是唯一能验证"用户实际能不能用"的手段。

xxx端到端流程已拆分为 10 个独立测试函数,在 Allure 报告中每步单独显示 #01~#10,方便精确定位失败位置:

示例:导入患者(tests/ui/test_xxx_workflow.py

import allure
import pytest
from actions import patient, _core

@pytest.fixture(autouse=True)
def _suite_labels():
    allure.dynamic.label("parentSuite", "06 系统测试")
    allure.dynamic.label("suite", "xxx端到端")

@allure.title("import xxx")
def test_xxx(
    qt_app, data_dir, report_dir, baseline_dir, tolerance, update_baseline
):
    patient.import_from_dir(qt_app, data_dir)
    _screenshot(qt_app, "xxxx", report_dir, baseline_dir,
                tolerance, update_baseline)

截图对比(非阻断):

def _screenshot(app, name, report_dir, baseline_dir, tolerance_pct, update_baseline=False):
    actual_path = Path(report_dir) / f"{name}.png"
    _core.capture(app, actual_path, tools_dir)

    # actual 图始终上传到 Allure
    allure.attach.file(str(actual_path), name=f"{name} (actual)",
                       attachment_type=allure.attachment_type.PNG)

    # 与 baseline 对比,差异超过容差只警告,不阻断后续步骤
    diff_pct = qt_ui_baseline.compare_images(actual_path, baseline_path, diff_path)
    if diff_pct > tolerance_pct:
        allure.attach.file(str(diff_path), name=f"{name} (diff {diff_pct:.1f}%)",
                           attachment_type=allure.attachment_type.PNG)
        warnings.warn(f"Screenshot diff={diff_pct:.2f}% > {tolerance_pct}%")

设计要点:

  • pywinauto UIA backend:通过控件的可访问性属性定位,不依赖坐标,分辨率变化不影响定位。
  • _06_run_system_tests.ps1 正式入口默认截图容差为 1%;直接运行 pytest 时 conftest.py 的默认值为 5%。Jenkins 以脚本入口传入的 1% 为准。
  • Nightly 使用 UPDATE_QT_UI_BASELINE,Release 使用 UPDATE_QT_BASELINE 控制是否更新 baseline,避免误覆盖。
  • 10 个测试函数共享 session 级 qt_app fixture,UI 状态在步骤间连续传递,等同于原来单函数设计

7、每一步如何实现

7.1 GitLab push 自动触发模块编译

当前覆盖范围

所有 5 个模块都已配置 GitLab CI,每次 push 到 develop / main 或提交 MR 时自动触发编译和测试:

触发条件对比:

值得关注的设计:xxx 普通测试 + 回归测试分离

xxx的 .gitlab-ci.yml 把测试拆成了两个 job:

# 每次 push / MR 都跑 — 排除 regression 标签的用例
test:xxx:
  script:
    - ctest -LE regression --output-junit test-results.xml
  only: [merge_requests, develop, main, schedules]

# 只在定时调度时跑 — 只跑 regression 标签的用例
test:xxx:regression:
  script:
    - ctest -L regression --output-junit regression-results.xml
  only: [schedules]

为什么这样设计:

  • 回归测试通常运行时间长(固定数据集、数值计算),不适合每次 push 都跑,会拖慢 MR 反馈速度。
  • ctest -L / -LE 按标签过滤,在 CMakeLists.txt 里给用例打标签:
add_test(NAME xxx COMMAND xxx --gtest_filter=xxx.Regression)
set_tests_properties(xxxPROPERTIES LABELS "regression")
  • 定时调度(GitLab Scheduled Pipeline)每晚触发一次,专门跑回归。这和 Jenkins Nightly 的职责形成互补:GitLab 做算法层回归,Jenkins 做端到端系统回归。
各模块测试覆盖方向
共同的流程结构

五个模块的 CI 流程完全一致:

push / MR
  -> Stage 1: build
       ① 检查 xx_ROOT / QT6_ROOT / TEST_DATA_DIR 是否存在(快速失败)
       ② cmake configure(-DBUILD_TESTS=ON)
       ③ cmake --build --parallel $(nproc)
  -> Stage 2: test
       ④ 设置 LD_LIBRARY_PATH(各模块依赖库不同)
       ⑤ ctest --output-on-failure --output-junit test-results.xml
       ⑥ artifacts: when: always → 失败时也上传 JUnit
建议后续改进

1. xxx 补充更多冒烟用例

当前 testxxx_smoke 只做最基础的初始化验证。可以补充:

  • xx连接初始化不崩溃
  • 主窗口对象可以创建(QT_QPA_PLATFORM=offscreen
  • 关键配置项可以被读取

2. 编译缓存,加快 MR 反馈速度

cache:
  key: "$CI_COMMIT_REF_SLUG-$CI_JOB_NAME"
  paths:
    - build/

目前每次都 rm -rf build/,MR 多时队列等待长。MR 检查阶段可以启用增量编译,develop push 再做全量。

3. 覆盖率上报

test:mevcore:
  script:
    - cmake ... -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
    - ctest ...
    - gcovr --xml coverage.xml
  coverage: '/^TOTAL.*\s+(\d+\%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

在 GitLab MR 页面直接看到覆盖率变化,帮助判断新增代码是否有对应测试。

8.2 Nightly 自动测试

触发:

每天凌晨 2:00

主要阶段:

Init
  -> GitLab CI Overview
  -> Checkout xxx
  -> Verify Product Outputs
  -> Unit Tests
  -> Integration Tests
  -> Regression Tests
  -> Qt UI Tests
  -> post(always): 汇总 Allure / JUnit / 日志 / 截图

重点设计:

  • 不合并 main。
  • 不打 tag。
  • 不触发打包。
  • 只验证 develop 当前状态。
  • 失败也要保留 Allure、JUnit、日志、截图。

8.3 Release 手动发版测试

Release Job 可配置:

参数作用
RUN_UNIT_TESTS是否跑单元测试
RUN_INTEGRATION_TESTS是否跑 API 集成测试
RUN_REGRESSION_TESTS是否跑数值回归
RUN_QT_UI_TESTS是否跑 Qt UI 系统测试
UPDATE_QT_BASELINE本次运行是否将截图覆盖到 baseline(布尔,默认 false)
RELEASE_VERSION指定版本号,最终规范化为一个 v... tag
TRIGGER_PACKAGE成功后是否触发打包

Release 的关键保护:

先所有仓库本地 merge 成功
  -> 再统一 push main
  -> 再统一 push tag

避免出现:

xx已 tag
xxx已 tag
xxxx merge 失败

这种半发布状态。

8.4 Package 从 tag 打包

Package 输入:

TAG = v2026.06.11.9

打包流程:

Init
  -> 校验 TAG / xxx_ROOT / Qt6_DIR / makensis
  -> Checkout all repos by TAG
  -> Build xxx
  -> Build + install xxx
  -> Build + install xxx
  -> Build + install xxx
  -> Build xxx
  -> Build xxx
  -> windeployqt + config staging
  -> NSIS xxx installer
  -> NSIS xxx installer
  -> Archive

关键点:

  • TAG 必须明确,不自动取最新 commit。
  • xx_ROOTQt6_DIR 与 CMake 变量同名。
  • makensis.exe 查找顺序:
MAKENSIS_EXE 参数
  -> PATH 中的 makensis.exe
  -> D:\Program Files (x86)\NSIS\makensis.exe

8、每个测试环境包含哪些

8.1 本地开发环境

用于开发者快速自测。

包含:

  • 本地源码。
  • 本地编译输出。
  • test.env.ps1
  • xxx测试数据。
  • 手动启动或脚本启动服务。

适合:

  • 单个脚本调试。
  • 单个接口验证。
  • UI 自动化脚本调试。
  • baseline 截图维护。

不适合:

  • 正式发版依据。
  • 多仓库一致性验证。

8.2 GitLab CI 环境

用于快速反馈。

包含:

  • Runner。
  • .gitlab-ci.yml
  • 单仓库 checkout。
  • 单元测试或基础构建。

适合:

  • 每次 push 自动检查。
  • MR 合并前检查。
  • 单仓库问题快速定位。

8.3 Jenkins develop 编译环境

用于生成 develop 最新 output。

包含:

  • 固定 workspace。
  • code/build/output 三层结构。
  • CMake、Qt、utils。
  • GitLab webhook。

适合:

  • 多仓库 develop 最新构建。
  • Nightly/Release 测试输入。

8.4 Jenkins Nightly 测试环境

用于无人值守自动测试。

包含:

  • develop output。
  • 自动测试脚本。
  • 测试数据。
  • xxxx。
  • xx。
  • Allure。
  • Qt UI GUI agent。

适合:

  • 每日健康检查。
  • 趋势观察。
  • 回归问题早发现。

8.5 Jenkins Release 环境

用于正式发版前验证。

包含:

  • develop output。
  • 全量或可选测试。
  • release-repos。
  • Git 凭据。
  • tag 权限。
  • 触发 Package 的能力。

适合:

  • 发版门禁。
  • main 合并。
  • tag 生成。
  • Package 触发。

89.6 Jenkins Package 环境

用于从 tag 生成安装包。

包含:

  • 独立 checkout。
  • CMake。
  • Qt。
  • utils。
  • NSIS。
  • dist/packages 归档目录。

适合:

  • 正式安装包构建。
  • 历史 tag 重打包。
  • 安装包归档。

9、现有测试用例体系

已经覆盖了哪些测试类型、每类测试解决什么问题、一个典型用例长什么样、后续新增用例应该放在哪一层。

10.1 测试用例分层

当前自动化测试可以按执行阶段分为:

阶段脚本测试类型主要目标
02 单元测试unit_tests.ps1GTest / 模块级测试验证函数、类、算法、解析逻辑
04 集成测试integration_tests.ps1API / 服务 / 配置 / 数据库验证多个模块协作是否正确
05 回归测试regression_tests.ps1固定数据 + 固定期望防止历史问题再次出现,数值结果稳定性
06 系统测试system_tests.ps1xxx GUI / UIA / 端到端流程验证真实用户流程是否可跑通
Qt UI baseline系统测试内触发截图基准对比验证关键界面视觉状态没有异常变化

注:services.ps1 用于检查或启动 服务,并可按需启动 xxx;它是测试环境准备阶段,因此测试报告编号从 集成测试 继续。

10.2 现有覆盖清单与空白


集成测试(04)

回归测试(05)
系统测试(06)

10.3 Qt UI baseline 用例

Qt UI baseline 是系统测试中的视觉验证。它用于发现:

  • 页面布局异常。
  • 控件遮挡。
  • 字体、颜色、尺寸异常变化。
  • 关键页面没有正确渲染。

目录建议:

code\tests\ui\baselines
  -> 标准基准图

reports\qt_ui
  -> actual
  -> diff
  -> failed

典型流程:

打开目标页面
  -> 等待页面稳定
  -> 截取 actual → 上传 Allure 附件
  -> 与 baseline 对比,生成 diff
  -> diff 超过容差 → 上传 diff 图 → 记录警告(不阻断测试)

截图对比设计要点:

  • system_tests.ps1 的默认容差为 1%(Jenkins 正式入口);直接调用 pytest 时默认值为 5%。应优先以流水线显式传入值为准。
  • diff 超过容差时不会让测试失败,仅发出 warnings.warn 并把 diff 图上传到 Allure,后续步骤继续执行。这是因为 Jenkins 节点渲染环境(DPI、字体渲染)与本地不完全一致,导致轻微差异不可避免。
  • 如果图片尺寸不同(例如窗口大小变化),跳过对比,仅保存 actual。

baseline 更新原则:

  • 普通测试不会覆盖已存在的 baseline;但当前实现会在 baseline 缺失时用本次 actual 自动创建,因此标准 baseline 应预先完整提交,且需要关注意外新增文件。
  • Jenkins Nightly 使用 UPDATE_QT_UI_BASELINE,Release 使用 UPDATE_QT_BASELINE(均默认 false);启用后脚本会同时传入 -ApproveBaseline-AllowBaselineUpdate
  • 命令行可用 --update-baseline 标志在本地手动触发更新。
  • 建议在固定分辨率、固定字体、固定 Windows 缩放比例的标准机器上更新。
  • baseline 需要进入 git 管理,否则不同电脑无法复现。

10.4 测试用例如何进入 Allure

Allure 报告使用三级层级结构对所有测试(含 GTest C++ 和 pytest Python)统一展示:

parentSuite(顶层)  →  suite(中层)  →  subSuite(叶层)  →  用例

各阶段映射:

parentSuitesuite来源
02 单元测试testxxxGTest JUnit XML → Allure JSON
04 集成测试01 DICOM / 02 患者管理 / … / 07 端到端流程pytest + allure-pytest
05 回归测试数值回归pytest + allure-pytest
06 系统测试xxx端到端pytest + allure-pytest

每个用例至少要表达四类信息:

信息Allure 表达方式目的
所属层级parentSuite / suite报告按阶段分组
关键步骤step / allure.step()失败时知道卡在哪一步
失败现场attachment保存截图、diff、response
视觉证据actual 截图附件每步完成后上传截图

pytest 示例(在 conftest.py 中用 autouse fixture 统一设置层级,避免每个文件重复写):

@pytest.fixture(autouse=True)
def _allure_suite():
    allure.dynamic.label("parentSuite", "04 集成测试")
    allure.dynamic.label("suite", "01 xxx")

10.5 后续新增测试用例模板

新增用例前先回答 6 个问题:

问题示例
测试目标是什么?防止 xxx导入成功但xxx列表不刷新
应该放在哪一层?系统测试,而不是单元测试
输入数据是什么?TEST_DATA_DIR\xx
成功断言是什么?xxx列表出现xx
失败时要保留什么?截图、UIA tree、service log、Allure step
是否稳定可重复?依赖 UIA 状态判断,不依赖固定坐标

推荐模板:

用例名称:
所属阶段:
测试目标:
前置条件:
测试数据:
执行步骤:
断言:
失败附件:
是否进入 Nightly:
是否进入 Release:

示例:

用例名称:导入xxx列表刷新
所属阶段:06 系统测试
测试目标:防止导入成功弹窗出现后xxx列表未刷新
前置条件:xxx已启动,服务可用
测试数据:TEST_DATA_DIR\xxx
执行步骤:
  1. 删除已有同名xxx
  2. 打开导入弹窗
  3. 输入 xxx路径
  4. 点击导入
  5. 等待成功弹窗
  6. 点击确认
  7. 搜索xxx
断言:
  - 成功弹窗出现
  - 列表出现 xxx
失败附件:
  - 当前截图
  - UIA tree
  - service log
  - Allure step
是否进入 Nightly:是
是否进入 Release:是

10、如何设计一个较好的自动化测试

10.1 分层设计

不要把所有测试都放进同一个阶段。

推荐结构:

快测试
  -> 单元测试
  -> 静态检查

中等测试
  -> API 集成
  -> 数据库
  -> WebSocket

慢测试
  -> 数值回归
  -> Qt UI
  -> 截图对比

发版测试
  -> 全量测试
  -> 合并
  -> tag
  -> package

10.2 数据可配置

不要把测试数据路径写死。

统一入口:

TEST_DATA_DIR

所有测试都从这个变量读取:

  • GTest DICOM 测试。
  • pytest 集成测试。
  • pytest 回归测试。
  • Qt UI 系统测试。

10.3 环境可验证

不要等测试跑一半才发现环境不对。

应在 Init 或 Verify 阶段提前检查:

  • xxx_ROOT
  • Qt6_DIR
  • xxx_DIR
  • TEST_DATA_DIR
  • makensis.exe
  • 服务.exe
  • 客户端.exe
  • 必需配置文件。

10.4 失败可诊断

失败不是只看红色。

需要保留:

  • Jenkins console log。
  • JUnit XML。
  • Allure results。
  • 服务日志。
  • Qt UI 截图。
  • baseline / actual / diff。
  • Package build log。

10.5 状态可追溯

正式发版需要知道:

  • 哪个 release version。
  • 哪些仓库。
  • 每个仓库哪个 commit。
  • 是否经过 Release 测试。
  • 是否成功生成 package。

建议后续引入:

release-manifest.json

示例:

{
  "version": "v2026.06.11.9",
  "createdAt": "2026-06-11 17:30:00",
  "repos": {
    "xxx": "f4736f3...",
    "xx": "d3767b0..."
  },
  "packageStatus": "pending"
}

11、我们方案的优点

11.1 分工清晰

GitLab CI      -> 快速反馈
Jenkins Build  -> develop 编译缓存
Nightly        -> 每日自动测试
Release        -> 发版门禁
Package        -> 从 tag 打包

每条流水线职责明确,避免一个 Job 承担所有事情。

12.2 不污染 develop 工作区

Release 合并/tag 在:

xxx-Release\release-repos

不再在:

xxx\code
...

这些 develop 编译目录中切分支。

12.3 支持复杂 Windows GUI 测试

Qt UI 测试运行在:

windows-gui agent

可使用 Windows UI Automation:

  • 启动 xxx。
  • 删除/导入xx。
  • ...
  • 截图对比。

12.4 报告体系较完整

当前报告包含:

  • Jenkins console。
  • JUnit。
  • Allure。
  • HTML GitLab CI Overview。
  • Qt UI actual/baseline/diff。
  • artifact 归档。

12.5 发版链路可控

Release 是手动触发。

只有测试成功后才:

merge main
tag
trigger package

更适合xxx软件这种需要谨慎发版的场景。

12、我们方案的不足和风险

12.1 依赖 Jenkins 节点环境

风险:

  • PATH 不一致。
  • Jenkins service 账户和桌面用户环境不同。
  • Qt、NSIS、CMake路径需要统一。

改进:

  • 参数化环境路径。
  • Init 阶段提前校验。
  • 节点环境文档化。
  • 关键工具优先从 PATH 查找,再 fallback。

12.2 GUI 测试稳定性依赖桌面会话

风险:

  • Jenkins agent 没有真实登录桌面。
  • RDP 断开影响截图。
  • 分辨率不同导致 baseline diff。

改进:

  • GUI agent 固定账号。
  • 固定分辨率。
  • baseline 只允许标准机器更新。
  • UIA 优先,不依赖坐标。

12.3 Package 全量构建较慢

原因:

  • 从 tag 独立 checkout。
  • 多仓库重新编译。
  • CMake + MSBuild 全量构建。
  • windeployqt 和 NSIS 打包耗时。

改进方向:

  • 可引入 CLEAN_PACKAGE_BUILD 参数。
  • 正式发版默认全量。
  • 调试打包允许增量。
  • 可评估 Ninja + 并行构建。

13.4 Tag 和 manifest 仍需继续完善

当前 Package 主要依赖 tag。

更理想:

Release 成功
  -> 生成 release-manifest.json
  -> Package 从 manifest 下拉选择

这样可以避免:

  • 用户不知道 tag。
  • tag 被误删。
  • 半发布 tag 被误用。
  • 多仓库 tag 不一致。

12.5 多仓库一致性需要强校验

风险:

  • 某个仓库 tag 缺失。
  • 某个仓库 main 合并失败。
  • 某次失败只打了部分 tag。

当前改进:

先全部本地 merge 成功
再统一 push main/tag

后续建议:

Package 前校验所有仓库均存在同一 tag
Release 成功后生成 manifest
Package 只从 manifest 选择

13、后续优化建议

13.1 Release Manifest

在 Release 成功后生成:

D:\JenkinsCache\workspace\xxx-Release\manifests\<tag>.json

记录:

  • release tag。
  • build number。
  • Jenkins URL。
  • 每个 repo commit。
  • 测试结果。
  • package 状态。

14.2 Package 下拉选择

通过 Jenkins Active Choices Parameter 或 Git Parameter:

PACKAGE_VERSION:
  v2026.06.11.9
  v2026.06.12.10

优先从 manifest 列表选择,而不是手填 tag。

14.3 增量打包模式

新增:

CLEAN_PACKAGE_BUILD = true

策略:

模式行为场景
true删除 build/output,干净构建正式发版
false保留 build,重新 configure,增量 build调试打包

14.4 Ninja 构建

新增:

CMAKE_GENERATOR = Ninja
CMAKE_BUILD_PARALLEL_LEVEL = 16

前提:

  • Jenkins agent 能找到 ninja.exe
  • Jenkins agent 能找到 MSVC cl.exelink.exe
  • 需要正确初始化 VS 编译环境。

14.5 环境标准化

建立 Jenkins 节点环境清单:

CMake
Visual Studio Build Tools
Qt
NSIS
Python
Allure
Git
xxx
xxx test data
xxx
GPU / CUDA
GUI desktop session

每个节点初始化时运行环境检查脚本。

更多推荐