第4章 CI流水线中集成单元测试:配置test阶段、使用artifacts收集测试报告
4.1 为什么需要专门的test阶段
在持续集成流水线中,单元测试必须拥有独立阶段,原因如下:
- 责任分离:构建阶段只负责产出可执行文件或库,测试阶段校验功能正确性,互不影响。
- 失败隔离:构建成功而测试失败时,开发者能立即定位到测试用例缺陷,而非构建环境问题。
- 报告持久化:GitLab CI 的
artifacts机制可自动收集测试产物(XML/HTML 报告),便于后续分析。 - 并行优化:测试阶段可拆分为多个并行 job,大幅缩短流水线总耗时。
最佳实践:始终将
unit test 定义为流水线的第三个阶段(build → lint → test),lint 阶段提前过滤低质量问题,避免浪费测试资源。
4.2 配置test阶段的完整流程
以下流程图展示了在一个典型PHP项目中,GitLab CI 从 build 到 test 阶段的产物流转关系:
配置 test 阶段分三步:定义阶段顺序、编写测试 job、声明 artifacts 收集规则。
4.2.1 在 .gitlab-ci.yml 中声明阶段
stages:
- build
- lint
- test
- deploy
4.2.2 编写测试 job 并配置 artifacts
以下示例是一个完整 PHP 项目的测试阶段配置,使用 vendor/bin/phpunit 执行测试,并收集 JUnit 和代码覆盖率报告:
unit_tests:
stage: test
image: php:8.2-cli
script:
- apt-get update && apt-get install -y unzip git
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-progress --no-interaction
- vendor/bin/phpunit --log-junit=junit.xml --coverage-clover=coverage.xml --coverage-html=coverage/
artifacts:
paths:
- junit.xml
- coverage.xml
- coverage/
reports:
junit: junit.xml
expire_in: 30 days
only:
- merge_requests
- main
关键字段说明:
artifacts.paths— 指定哪些文件/目录会被持久化。artifacts.reports.junit— 特殊类型,GitLab 会自动解析 JUnit XML 并展示在 MR “测试”标签页。expire_in— 避免 artifacts 无限占用存储,建议设为 30 天或更短。only— 只在 MR 和 main 分支运行,节省 CI 资源。
4.3 调试测试阶段常见问题
| 现象 | 原因 | 解决方案 |
|---|---|---|
| artifacts 未生成 | 测试 job 因脚本错误提前退出 | 在 job 末尾添加 ls -la junit.xml coverage.xml 确认文件存在 |
| JUnit 报告在 MR 中不可见 | report 路径错误或文件格式非标准 JUnit | 本地执行 vendor/bin/phpunit --log-junit=junit.xml 验证输出 |
| artifacts 过大导致流水线缓慢 | 包含了不必要的 vendor 目录 | 只收集 junit.xml coverage.xml coverage/,排除其他路径 |
| 并行测试 job 互相覆盖 artifacts | 多个 job 使用相同文件名但无隔离 | 使用 ${CI_JOB_NAME} 动态命名,如 junit-${CI_JOB_NAME}.xml |
4.4 性能与稳定性优化建议
- 使用缓存而非 artifacts 传递依赖:
composer install的依赖应该由cache机制处理,artifacts 只传递测试报告。 - 开启测试并行:将 test 阶段拆分为多个 job(如 api_test、unit_test、integration_test),每个 job 单独收集报告。
- 设置合理的 expire_in:对于 MR 的 artifacts 建议
expire_in: 7 days,仅 main 分支保留 30 天。 - 使用 GitLab 内置测试报告解析:在
artifacts:reports:中声明junit、cobertura或coverage_report,无需额外解析脚本。
4.5 常见错误与避坑指南
陷阱1:artifacts 不包含父目录
如果测试报告生成在
如果测试报告生成在
coverage/html/ 内部,paths: 中必须写 coverage/ 而不能只写 coverage/html/index.html,否则整个目录结构会丢失。
陷阱2:多个 job 使用相同 artifacts 名称
并行 job 若都输出
并行 job 若都输出
junit.xml,后完成的 job 会覆盖先完成的。推荐使用 ${CI_JOB_ID} 或 ${CI_NODE_INDEX} 命名。
推荐方案:始终在
script 的最后一行执行 ls -la ./ 并观察 CI 日志,确认报告文件已生成至正确路径。
4.6 完整 test 阶段配置示例(多语言参照)
| 语言/框架 | 测试命令 | 报告收集 |
|---|---|---|
| Python (pytest) | pytest --junitxml=junit.xml --cov=./ | reports: junit: junit.xml |
| JavaScript (Jest) | npx jest --ci --reporters=default --reporters=jest-junit | reports: junit: junit.xml |
| Java (Maven) | mvn test (自动生成 surefire-reports) | paths: target/surefire-reports/*.xml |
| Go | go test -v -json ./... > report.json | paths: report.json (需自行转换 JUnit) |
无论使用何种语言,核心原则不变:测试阶段必须独立于构建阶段,且 artifacts:paths 只打包报告文件而不打包源码或二进制,这样才能实现轻量、可复用的测试报告收集。