第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阶段与artifacts数据流 Build composer install artifacts Test phpunit artifacts: reports 报告存储 GitLab UI / 下载 测试阶段收集的关键文件 • junit.xml — PHPUnit 生成的 JUnit 格式报告 • coverage.xml — PHPUnit 代码覆盖率 Clover 格式 • html/ — 可下载的 HTML 覆盖率报告(可选)

配置 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 性能与稳定性优化建议

  1. 使用缓存而非 artifacts 传递依赖:composer install 的依赖应该由 cache 机制处理,artifacts 只传递测试报告。
  2. 开启测试并行:将 test 阶段拆分为多个 job(如 api_test、unit_test、integration_test),每个 job 单独收集报告。
  3. 设置合理的 expire_in:对于 MR 的 artifacts 建议 expire_in: 7 days,仅 main 分支保留 30 天。
  4. 使用 GitLab 内置测试报告解析:在 artifacts:reports: 中声明 junit、cobertura 或 coverage_report,无需额外解析脚本。

4.5 常见错误与避坑指南

陷阱1:artifacts 不包含父目录
如果测试报告生成在 coverage/html/ 内部,paths: 中必须写 coverage/ 而不能只写 coverage/html/index.html,否则整个目录结构会丢失。
陷阱2:多个 job 使用相同 artifacts 名称
并行 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-junitreports: junit: junit.xml
Java (Maven)mvn test (自动生成 surefire-reports)paths: target/surefire-reports/*.xml
Gogo test -v -json ./... > report.jsonpaths: report.json (需自行转换 JUnit)

无论使用何种语言,核心原则不变:测试阶段必须独立于构建阶段,且 artifacts:paths 只打包报告文件而不打包源码或二进制,这样才能实现轻量、可复用的测试报告收集。