第11章 建立测试体系与GitLab持续集成
截至v0.5,项目已经积累了多类检查,但它们分散在各章记录中。本章建立持续集成(Continuous Integration,CI)。CI把可重复的检查纳入项目工具和GitLab流水线,为合并请求产生质量证据。
需求和风险
→ 测试用例
→ 本地统一命令
→ GitLab流水线
→ 合并请求检查
→ 发布候选版本
持续集成不能代替真实硬件和用户场景。它适合自动发现确定性错误;板卡、网络、功耗和现场效果仍需要明确的人工或硬件在环验证。
人工检查表适合低频发布,但频繁合并会使重复检查容易遗漏。CI把确定性检查交给统一环境执行,从而较早发现回归。它需要维护运行器和依赖,也可能受到不稳定测试影响,因此不能代替工程判断。
完成本章后,应当能够:
- 从需求、接口和风险设计分层测试;
- 区分单元、契约、集成、系统和场景验证;
- 为AI任务预先写出可执行验收测试;
- 使用一个本地入口复现持续集成命令;
- 阅读并修改
.gitlab-ci.yml; - 理解流水线、阶段、作业、运行器和构建物;
- 保护持续集成变量并限制外部分支取得秘密;
- 根据第一条有效错误定位流水线失败;
- 防止AI通过删除测试或放宽规则制造“通过”;
- 把流水线状态接入主分支保护;
- 形成
v1.0.0-rc.1发布候选。
11.1 从需求和风险形成测试
11.1.1 每项测试都要有对象
测试不是为了增加文件数量,而是验证一个已经声明的行为或风险:
PR-02 事件送达
→ FR-004 事件上报
→ API-001 接收接口
→ IT-004 合法事件被接受
→ IT-005 重复事件不重复写入
→ ST-002 真实设备端云闭环
在docs/verification/test-matrix.md维护:
| 测试ID | 来源 | 层级 | 输入/条件 | 预期 | 执行方式 | 证据 | 状态 |
|---|---|---|---|---|---|---|---|
| CT-001 | EVENT-001 | 契约 | 合法示例 | Schema通过 | 自动 | CI日志 | Active |
| IT-005 | FR-004 | 集成 | 重复event_id | 单条记录 | 自动 | 测试报告 | Active |
| HT-003 | NFR-003 | 实机 | model-0.3.0 | RAM在预算内 | 人工/设备 | 资源报告 | Active |
测试状态可以是Planned、Active、Blocked或Retired。删除或停用测试必须说明对应需求是否也已改变。
11.1.2 测试层级
| 层级 | 主要对象 | 是否适合每次合并请求自动运行 |
|---|---|---|
| 静态检查 | 文件、格式、配置、秘密模式 | 是 |
| 单元测试 | 纯函数、状态机、缓冲逻辑 | 是 |
| 契约测试 | JSON模式、构建物清单、接口样例 | 是 |
| 主机集成测试 | 服务器、存储、模型包、回放 | 是 |
| 固件构建 | 多配置编译、资源上限 | 是 |
| 硬件在环 | 开发板、传感器、真实推理 | 视课程设备条件 |
| 场景测试 | 用户、安装和现场网络 | 否,按版本执行 |
越接近底层的测试通常越快、越稳定;越接近真实场景的测试覆盖越完整但成本越高。项目需要组合,而不是只选择一种。
11.2 为AI变更先建立验收测试
11.2.1 用失败示例限制AI范围
假设议题要求“缺少model_version的事件必须被拒绝”。在让AI改服务器前,先增加一个失败测试:
给定:有效事件样例
当:删除model_version
则:Schema检查失败,服务器不写入
提交可以分为:
test(contract): reject event without model version
fix(server): enforce model version in event ingestion
第一提交描述缺陷的可执行表现,第二提交修复实现。AI更难通过增加无关功能来绕开问题。
11.2.2 测试本身也需要评审
检查:
- 测试是否真的经过目标代码;
- 断言是否检查关键结果;
- 测试失败时是否给出可定位信息;
- 是否使用固定、脱敏输入;
- 是否依赖本地时间、随机顺序或外部网络;
- 是否为了通过而改变验收值;
- 是否只测试AI刚刚实现的写法,而不是需求行为。
AI可以生成测试框架,但学生必须故意破坏实现一次,确认测试能够捕获错误。
11.3 统一本地验证入口
tools/project.py增加:
python tools/project.py validate
python tools/project.py test-unit
python tools/project.py test-contract
python tools/project.py test-server
python tools/project.py test-model-package
python tools/project.py test-replay
python tools/project.py build-firmware
python tools/project.py release-check
release-check按固定顺序调用发布所需的非硬件检查,并在任一步失败时返回非零退出码。持续集成依据退出码判断作业成功或失败。
11.3.1 命令必须从干净环境可运行
验证:
- 新建或使用干净工作目录;
- 克隆指定提交;
- 按锁定文件安装依赖;
- 取得经过核对的模型构建物;
- 执行统一命令;
- 检查没有依赖个人全局配置。
如果命令只在开发者电脑上成功,应先修复环境说明或依赖锁定,再接入持续集成。
11.3.2 固定工具版本
项目保存:
requirements-ci.txt或等价锁定文件
固件工具链版本
板卡支持包版本
模型运行时版本
课程CI镜像版本
“安装未固定版本”会使同一提交在不同日期产生不同结果。依赖升级建立独立议题和合并请求,并重新执行相关测试。
11.4 认识GitLab流水线
早期软件项目常在开发结束后集中测试。问题会在较多变更叠加后才被发现,因而难以定位。持续集成把测试提前到每次合并附近,使失败能够关联到较小的差异。
持续集成依赖稳定环境和确定性测试。它也会占用计算资源,并增加流水线维护成本。因此,项目应把可重复检查自动化,把现场判断保留给实机和用户验证。
GitLab持续集成由项目根目录.gitlab-ci.yml定义:
流水线 Pipeline
├─ validate阶段
│ ├─ 文件与契约作业
│ └─ 敏感信息作业
├─ test阶段
│ ├─ 单元测试作业
│ ├─ 服务器测试作业
│ └─ 模型包测试作业
├─ build阶段
│ └─ 固件构建作业
└─ package阶段
└─ 发布候选构建物作业
- 作业执行一组命令;
- 阶段规定作业的大体先后;
- 同一阶段的作业可以并行;
- 运行器提供实际执行环境;
- 构建物保存日志、报告和编译结果。
流水线运行的是一个确定Git提交。查看失败时先记录CI_COMMIT_SHA,不要只写“最新代码失败”。
11.5 编写第一份.gitlab-ci.yml
课程环境卡提供经过批准的持续集成镜像和运行器。下面结构需要把镜像变量替换为校内部署的实际值:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
stages:
- validate
- test
- build
- package
default:
image: "$COURSE_CI_IMAGE"
before_script:
- python --version
- python tools/project.py ci-info
validate:
stage: validate
script:
- python tools/project.py validate
- python tools/project.py test-contract
- python tools/project.py test-model-package
secret-check:
stage: validate
script:
- python tools/secret_check.py
unit-test:
stage: test
script:
- python tools/project.py test-unit
artifacts:
when: always
reports:
junit: build/reports/unit-tests.xml
server-test:
stage: test
script:
- python tools/project.py test-server
artifacts:
when: always
reports:
junit: build/reports/server-tests.xml
replay-test:
stage: test
script:
- python tools/project.py test-replay
artifacts:
when: always
paths:
- build/reports/replay-summary.json
firmware-build:
stage: build
script:
- python tools/project.py fetch-model model-0.3.0
- python tools/project.py build-firmware --release
- python tools/project.py firmware-budget-check
artifacts:
paths:
- build/firmware/
- build/reports/firmware-size.json
release-package:
stage: package
script:
- python tools/project.py package-candidate
artifacts:
paths:
- build/release/
rules:
- if: '$CI_COMMIT_TAG'
该示例表达流水线结构,不保证直接适用于所有校内GitLab版本。提交前使用校内持续集成语法检查,并按实际报告路径调整。
11.6 控制流水线触发范围
11.6.1 合并请求和主分支都要运行
workflow.rules规定:
- 创建或更新合并请求时运行;
- 合并到默认分支后再次运行;
- 创建标签时运行发布候选或发布作业。
合并请求流水线验证源分支内容。若校内GitLab启用“合并结果流水线”,还可以验证源分支与目标分支合并后的结果;是否启用由课程管理员根据版本和运行器资源配置。
11.6.2 不要为节省时间跳过关键检查
可以使用changes只在相关文件变化时运行昂贵作业,但要考虑间接影响。例如:
- 修改模型清单必须重新构建固件;
- 修改公共头文件影响多个固件目标;
- 修改事件模式影响设备、服务器和用户端;
- 修改工具链配置影响所有检查。
若依赖关系难以准确表达,初期宁可运行完整基础流水线。
11.7 管理持续集成变量和秘密
11.7.1 不把秘密写进YAML
.gitlab-ci.yml只引用变量:
script:
- python tools/project.py integration-test
variables:
EVENT_SERVER_URL: "$CI_TEST_EVENT_SERVER_URL"
真实令牌在GitLab项目设置中保存为受保护、掩码或文件类型变量,具体能力取决于校内版本。
11.7.2 合并请求流水线默认不使用生产秘密
来自普通任务分支或分叉仓库的代码尚未通过评审,不应自动取得高权限变量。基础合并请求流水线使用:
- 本地测试服务器;
- 临时数据库;
- 无权限的测试凭据;
- 脱敏固定数据;
- 不访问真实用户环境的网络。
需要真实设备或受控服务的作业,应设置为受保护分支、受控运行器或人工批准,并限制执行者。
11.7.3 敏感信息检查
tools/secret_check.py至少检查:
- 常见令牌和私钥头;
.env、本地秘密头文件;- 内部服务令牌模式;
- 数据文件中不应出现的身份字段;
- 模型或数据下载URI中的查询令牌。
扫描命中后由人工判断。不能为消除失败而把真实秘密加入允许列表;应先吊销、删除并评估历史影响。
11.8 产生可审查的测试报告和构建物
11.8.1 日志应帮助定位
每个作业输出:
Git提交
工具版本
模型版本和散列
契约版本
执行命令
测试数量
失败摘要
日志不输出完整令牌、原始个人数据或无限长度的调试内容。
11.8.2 构建物不是永久发布
流水线构建物可以保存:
- 测试报告;
- 固件二进制;
- 映射文件和资源报告;
- 契约检查结果;
- 发布包清单。
它们有保留期限,也可能受访问控制。正式发布还要形成带版本、散列值和说明的发布记录。某次流水线的下载链接不能代替发布记录。
11.8.3 发布构建物绑定提交
固件构建信息至少嵌入:
产品版本
Git提交
构建时间或可复现构建标识
模型版本
策略版本
硬件目标
用户反馈问题时,设备日志能够指出实际运行组合。
11.9 分析流水线失败
11.9.1 从第一条有效错误开始
处理顺序:
- 记录流水线和提交标识;
- 找到第一个失败作业;
- 找到该作业第一条实际错误;
- 判断是代码、测试、依赖、环境还是服务问题;
- 在相同提交上本地复现;
- 建立最小修复;
- 运行失败作业及受影响检查;
- 推送后观察新流水线。
后续大量错误可能由第一项失败连锁产生。把全部日志交给AI并要求“修好持续集成”,容易造成大范围修改。
11.9.2 给AI提供受限故障上下文
任务:分析unit-test作业的第一条失败。
提交:<实际SHA>
失败命令:python tools/project.py test-unit
第一条错误:<原文>
允许读取:
- 失败测试文件
- 对应实现文件
- 相关接口头文件
先回答:
1. 错误发生在哪一层;
2. 它是否由当前MR引入;
3. 最小修复候选;
4. 需要重新运行的测试。
不修改CI配置,不删除测试,不更改验收值,不安装新依赖。
人工确认原因后再允许实施。
11.9.3 不接受以下“修复”
- 把失败作业设为
allow_failure; - 注释测试;
- 捕获所有异常后返回成功;
- 扩大数值容差但没有依据;
- 固定写入期待输出;
- 把真实测试替换为只检查函数存在;
- 降级秘密扫描规则;
- 在YAML中写入个人环境路径。
这些变化让流水线变绿,却没有修复产品。
11.10 处理不稳定测试
同一提交在相同环境中时而成功、时而失败的测试称为不稳定测试。常见原因:
- 依赖系统时间;
- 随机种子未固定;
- 端口或线程竞态;
- 测试共享数据库状态;
- 外部网络服务;
- 硬件连接和串口争用。
处理:
- 保存至少两次相反结果;
- 建立缺陷议题;
- 隔离共享状态;
- 固定输入、时间和随机种子;
- 把外部服务替换为受控测试服务;
- 修复后重复运行。
关键合并条件中的测试不能长期通过“重跑直到成功”处理。
11.11 接入硬件在环验证
若课程具备专用开发板和受控运行器,可以建立人工触发作业:
hardware-in-loop:
stage: test
script:
- python tools/project.py hil-test --board "$COURSE_BOARD_ID"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
tags:
- course-hardware
硬件在环作业要处理:
- 独占设备;
- 烧录失败恢复;
- 串口超时;
- 板卡和传感器身份;
- 测试后清理;
- 物理安全;
- 日志和数据权限。
没有专用设施时,继续使用规范化人工记录。不能用一条总是成功的“占位作业”代替实机检查。
11.12 设置合并条件
教师或维护者在GitLab中配置:
[ ] main禁止直接推送
[ ] 合并请求流水线必须成功
[ ] 所有阻塞讨论必须解决
[ ] 至少一名同伴评审
[ ] 模型、硬件或数据变化请求对应责任人评审
[ ] 禁止合并存在冲突的分支
[ ] 合并后主分支流水线再次运行
自动检查负责机械规则,评审者负责需求、架构、风险和证据判断。两者缺一不可。
11.13 形成发布候选版本
11.13.1 发布候选检查
运行:
python tools/project.py release-check
并完成:
[ ] 所有v1.0范围需求有实现和测试
[ ] 契约、数据、模型和硬件清单一致
[ ] 固件发布构建通过资源预算
[ ] 服务器和回放测试通过
[ ] 真实设备端云验证完成
[ ] 敏感信息检查通过
[ ] README.md和已知限制更新
[ ] 未关闭的阻塞议题为0
11.13.2 创建预发布标签
语义化版本中,v1.0.0-rc.1表示第一个1.0发布候选:
git switch main
git pull --ff-only
git tag -a v1.0.0-rc.1 -m "v1.0.0 release candidate 1"
git push origin v1.0.0-rc.1
试用中发现问题后,以新提交修复并创建rc.2,不移动rc.1标签。
11.14 本章小结
本章把分散的工程检查组织为分层测试。GitLab持续集成为合并请求、主分支和版本标签生成质量证据。流水线验证契约、代码、模型包和固件构建。硬件与场景验证继续由真实设备和规范记录完成。
AI可以生成测试、解释第一条实际错误和实施小范围修复,但不能删除检查、放宽验收或隐藏失败。v1.0.0-rc.1为下一章的小规模试用、开源审查和正式发布提供了固定基线。
11.15 综合实践
- 从本组一项需求和一项风险出发,建立测试追踪。分别设计自动测试、实机测试和场景验证,并说明三者边界。
- 为项目设计最小CI流水线。选择阶段、作业、构建物和合并条件,说明每项检查为什么适合自动执行。
- 有意引入一个安全的契约或构建错误。根据流水线第一条有效错误完成定位,并形成最小修复合并请求。
- 选择一项硬件在环检查,设计触发条件、设备身份、结果记录和失败处理。说明它为什么不能并入普通运行器。
- 评审当前合并门禁,提出一项能够提高质量且成本可接受的改进,并用一次实际流水线结果验证。
11.16 拓展阅读
- 校内GitLab持续集成与持续交付、合并请求流水线和作业构建物文档;
- 项目所用测试框架的官方文档;
- 语义化版本2.0.0规范。