自动化测试系统
- 自动化测试是什么
自动化测试是指利用脚本和工具代替人工重复执行测试步骤、自动验证结果并生成报告的过程。
它的核心目标不是”完全取代人工测试”,而是:
- 把高频、重复、稳定的验证动作从人工中解放出来。
- 在每次代码变更后尽快发现问题,而不是等到发版前。
- 降低回归验证成本——同样的用例可以无限次免费重跑。
- 固化测试环境、测试数据和测试报告,消除”我这里没问题”。
- 为发版提供可追溯的客观证据,而不依赖人的记忆和判断。
换句话说:自动化测试解决的是人工测试无法规模化的问题,不是要证明软件没有 bug。
什么情况适合自动化
| 适合自动化 | 不适合自动化(或收益低) |
|---|---|
| 高频执行、每次发版都要验证 | 只跑一次、需求还在快速变化 |
| 步骤固定、结果有明确判断标准 | 需要主观判断(美观、易用性) |
| 回归验证、历史 bug 防复现 | 探索性测试、边界挖掘 |
| 数值计算正确性 | 首次验证新功能是否符合预期 |
| 多模块集成后的接口联通性 | 极少执行、维护成本高于节省成本 |
自动化测试分层(测试金字塔)
不同层级的测试,速度、范围、维护成本差异很大。合理的自动化体系是”下多上少”——越底层的测试越多,越顶层的越精简:
┌────────────┐
│ 系统测试 │ 最慢、最脆、覆盖端到端流程
┌─┴────────────┴─┐
│ 回归测试 │ 固定数据 + 固定期望,防止历史问题复现
┌─┴────────────────┴─┐
│ 集成测试 │ 模块间接口、API、数据库
┌─┴────────────────────┴─┐
│ 单元测试 │ 最快、最稳、覆盖函数/算法局部逻辑
┌─┴────────────────────────┴─┐
│ 静态检查 / 构建验证 │ 编译通过、依赖完整、配置有效
└────────────────────────────┘
各层在我们项目中的对应:
| 层级 | 关注点 | 工具 | 在我们项目中 |
|---|---|---|---|
| 静态检查 / 构建 | 编译成功、依赖完整 | CMake、PowerShell | 模块编译 Job |
| 单元测试 | 函数、类、算法局部逻辑 | GTest (C++) | unit tests |
| 集成测试 | 模块间接口、HTTP API、WebSocket、数据库 | pytest、requests | integration tests |
| 回归测试 | 固定数据、数值结果稳定性 | pytest、golden data | regression tests |
| 系统测试 | 真实 UI、端到端流程、截图对比 | pywinauto、Windows UIA | system tests |
| 构建打包验证 | 从源码到安装包的完整可交付链路 | CMake、NSIS、Jenkins | Package 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 CI | GitHub Actions | Azure DevOps | Jenkins | 纯脚本 |
|---|---|---|---|---|---|
| 与 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 系、不支持多标签页、不适合后端联调场景 |
| Puppeteer | Google 出品,专门控制 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,脚本语言多 | 商业授权费用高,学习曲线陡 |
| pywinauto | Python 封装 Windows UIA / MSAA,开源 | 免费、与 pytest 完美集成、适合任何 Windows 程序 | 依赖可访问性树,Qt 控件需要正确暴露 UIA 属性 |
| WinAppDriver | 微软出品,基于 WebDriver 协议控制 Windows 应用 | 协议标准化、可配合 Appium 使用 | 配置步骤多,微软已停止主动维护 |
| TestComplete | SmartBear 商业全功能 UI 测试平台 | 支持 Web + 桌面 + 移动,录制回放 | 价格高、体量重 |
| Ranorex | 商业桌面 UI 测试工具 | 录制回放、报告完整 | 商业授权、生成的脚本可读性差 |
| AutoIt | Windows 脚本语言,模拟键鼠操作 | 轻量、上手快 | 不基于控件识别,依赖坐标/图像,极易因分辨率变化而失效 |
| SikuliX | 基于图像识别控制 UI | 不需要源码访问,任何界面都能用 | 对分辨率/DPI/缩放比例极度敏感,稳定性差 |
为什么我们选择 pywinauto
我们的桌面客户端 Qt6 Widgets 应用,运行在 Windows 上。工具选型考量:
| 考量点 | Squish | pywinauto(我们的选择) |
|---|---|---|
| 成本 | 商业授权,价格较高 | 完全免费开源 |
| 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 Pipeline | Jenkins 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 字段,保留在 GitLab | archiveArtifacts,保留在 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 在哪里定义
| Job | Pipeline 文件 | 位置 |
|---|---|---|
| GitLab push 编译验证 | .gitlab-ci.yml | 各模块仓库根目录 |
| Nightly 自动测试 | Jenkinsfile | xxx |
| Release 发版门禁 | JenkinsfileRelease | xxx |
| Package 打包 | JenkinsfilePackage | xxx |
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等模块 Job | GitLab push webhook | develop | 编译验证,快速反馈 |
xxx-Nightly | 每晚定时 | develop | 自动测试,出报告,不动 main |
xx-Release | 手动触发 | develop -> main | 发版前自动测试,成功后合并、tag |
xxx-FromTag | Release 触发或手动 | 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_appfixture,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_ROOT和Qt6_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.ps1 | GTest / 模块级测试 | 验证函数、类、算法、解析逻辑 |
04 集成测试 | integration_tests.ps1 | API / 服务 / 配置 / 数据库 | 验证多个模块协作是否正确 |
05 回归测试 | regression_tests.ps1 | 固定数据 + 固定期望 | 防止历史问题再次出现,数值结果稳定性 |
06 系统测试 | system_tests.ps1 | xxx 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(叶层) → 用例
各阶段映射:
| parentSuite | suite | 来源 |
|---|---|---|
02 单元测试 | testxxx | GTest 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_ROOTQt6_DIRxxx_DIRTEST_DATA_DIRmakensis.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.exe、link.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
每个节点初始化时运行环境检查脚本。
更多推荐

所有评论(0)