本文环境
| 组件 | 版本 / 说明 |
|---|---|
| 极狐GitLab | 17.0 及以上(JihuLab.com 与私有化部署均适用) |
| Runner 执行器 | Docker |
| 示例语言 | Go 1.22、Node.js 20 |
| 关键关键字 | artifacts:reports:junit、coverage、artifacts:reports:coverage_report |
一、先说现象:三种最常见的"配了但不显示"
现象 A:流水线跑完了,测试也执行了,合并请求里的「测试摘要」面板是空的,鼠标点开只显示"显示详情"后依旧没有条目。
现象 B:流水线作业列表里,某个作业名称旁边出现一个解析错误指示符,鼠标悬停提示 JUnit XML 解析错误。
现象 C :合并请求小部件里根本没有覆盖率百分比,而流水线日志里明明打印了 coverage: 82.5% of statements 这类输出。
这三个现象的根因完全不同:A 多半是产物过期或超出体积限制,B 是 XML 本身格式有问题,C 是正则没匹配上。不要一上来就改测试框架,先按下面的顺序定位。
二、定位:JUnit 报告与覆盖率报告是两套东西
这是最容易踩的第一个坑,官方文档的表述很直接:覆盖率报告与覆盖率可视化读取不同的输入、产生不同的输出,并且不共享配置,配置其中一种并不会启用另一种。
| 你想要的效果 | 应该配置 | 输入来源 |
|---|---|---|
| MR 小部件里的覆盖率百分比 | coverage(正则) |
作业日志中匹配到的第一个数字 |
| 流水线/作业列表的覆盖率 | coverage(正则) |
同上 |
| MR 差异里的逐行注释 | artifacts:reports:coverage_report |
Cobertura 或 JaCoCo XML 产物 |
| 覆盖率徽章、CSV 导出、历史图表 | coverage(正则) |
同上 |
| Coverage-Check 审批规则(专业版及以上) | coverage(正则) |
同上 |
踩坑 1 :只配了
artifacts:reports:coverage_report,然后到处找"为什么 MR 上没有百分比"。逐行注释来自 XML 报告,百分比来自日志正则,两者不是一回事,想同时拿到就必须两个关键字都配。
还有一个容易被忽略的差异:coverage 不记录子流水线的数据,而 coverage_report 会记录。如果你的测试跑在子流水线里,就别指望 coverage 给出百分比。
三、九步排查与根治
步骤 1:确认测试框架输出的是 JUnit XML
单元测试报告只认 JUnit XML 格式,其他格式不会被解析。以 Go 为例,需要先装转换工具:
bash
# go-junit-report 把 go test 的输出转成 JUnit XML
go install github.com/jstemmer/go-junit-report/v2@latest
yaml
go-test:
stage: test
image: golang:1.22
script:
- go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml
artifacts:
when: always
paths:
- report.xml
reports:
junit: report.xml
-set-exit-code 很关键:JUnit 报告本身不会影响作业状态,要让测试失败时作业真的失败,脚本必须以非零状态退出。
步骤 2:检查 junit 路径写法是否踩了"目录"红线
junit 支持单文件名、glob 模式、数组以及它们的组合:
yaml
# 单文件
reports:
junit: report.xml
# glob 模式
reports:
junit: test-results/**/*.xml
# 数组
reports:
junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]
# 组合
reports:
junit: [rspec.xml, test-results/TEST-*.xml]
踩坑 2 :不支持目录写法 。写成
junit: test-results或junit: test-results/**都不会被识别,必须落到具体文件名或文件匹配模式上。
步骤 3:确认文件体积没超限
单个 JUnit 文件必须小于 30 MB,作业内所有 JUnit 文件总和小于 100 MB。超限会导致报告解析不出来,表现为面板空白。
bash
# 本地先自查体积
find . -name "*.xml" -exec ls -lh {} \; | awk '{print $5, $9}'
步骤 4:给报告产物设置合理的过期时间
产物过期会让"测试摘要"面板变空,官方文档明确把"报告产物已过期"列为首要原因。默认值偏短,建议显式声明:
yaml
artifacts:
when: always
expire_in: 1 week
paths:
- report.xml
reports:
junit: report.xml
踩坑 3 :只配
reports:junit而不配paths,报告可以被解析但无法在产物里浏览,排查时看不到原始 XML,只能反复跑流水线。两个都配上最省事。
步骤 5:确认目标分支有基线数据
如果目标分支上从未跑过生成测试报告的流水线,就没有可比较的基线,合并请求里会完全看不到「测试摘要」面板。
bash
# 在默认分支上手动跑一次流水线补基线
git checkout main && git commit --allow-empty -m "chore: generate test report baseline" && git push
步骤 6:验证 XML 本身能被解析
作业名旁出现解析错误指示符,说明 XML 有格式问题。先把产物抓下来本地验一遍:
bash
# 用 python 快速校验 XML 是否闭合、属性是否合法
python3 -c "import xml.etree.ElementTree as ET,sys; ET.parse(sys.argv[1]); print('XML OK')" report.xml
bash
# 常见错误:未转义的特殊字符导致解析失败
grep -n "&" report.xml | head
JUnit XML 里被解析的元素其实很有限:testcase 的 classname(显示为套件名)、name(测试名)、file(文件路径)、time(执行时间),以及 failure/error/skipped 的元素内容。testsuite 上的 tests、failures、errors、timestamp 属性不会被解析,别指望靠它们显示总数。
步骤 7:检查是否存在重复的测试名称
如果 XML 里有重名测试,只有第一条会被采用,其余被忽略,表现为"结果比预期少"。
bash
# 统计重复的 testcase 名称
grep -o 'name="[^"]*"' report.xml | sort | uniq -c | sort -rn | head
步骤 8:配 coverage 正则,让百分比显示出来
coverage 关键字的值是一个正则,极狐GitLab 会扫描作业日志,提取第一个匹配到的数字并存储。
yaml
go-test:
stage: test
image: golang:1.22
script:
- go test -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out | tail -1
coverage: /total:\s+\(statements\)\s+\d+\.\d+%/
artifacts:
paths:
- coverage.out
踩坑 4 :正则里必须能匹配到你日志里真实出现 的那行。
go tool cover -func最后一行输出是total: (statements) 82.5%,正则写成/coverage: \d+%/就永远匹配不上,MR 上自然没有百分比。改正则前先在日志里grep一遍确认文本。
Node.js 侧同理:
yaml
jest-test:
stage: test
image: node:20
script:
- npm ci
- npx jest --coverage --coverageReporters=text-summary --coverageReporters=cobertura
coverage: /All files[^|]*\|\s*[\d.]+/
artifacts:
when: always
paths:
- reports/
reports:
junit: reports/junit.xml
coverage_report:
coverage_format: cobertura
path: reports/cobertura-coverage.xml
步骤 9:配 coverage_report,让 MR 差异出现逐行注释
yaml
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: reports/cobertura-coverage.xml
逐行注释只出现在合并请求差异中发生变更的文件上------没改动的文件即使报告里有覆盖率数据,也不会被注释。这是设计如此,不是配置错误。
踩坑 5 :
coverage与coverage_report可能给出不一致的数字。小部件的百分比来自日志正则,注释来自 XML 报告。必须从同一次测试运行中生成两者,否则你在看两份不同基准的数据。
四、验证结果
配置完成前后对比:
| 检查项 | 配置前 | 配置后 |
|---|---|---|
| MR 测试摘要面板 | 空白或缺失 | 展示新增/既有/已解决失败分类 |
| 作业列表覆盖率 | 无 | 显示百分比及与目标分支差值 |
| MR 差异行级注释 | 无 | 变更文件显示 covered/missed |
| 失败测试定位 | 翻作业日志 | 面板内直接查看详情与截图 |
| Coverage-Check 审批 | 不可用 | 覆盖率下降时自动要求审批(专业版及以上) |
一个额外收益:对于过去 14 天内在默认分支上失败的测试,面板会提示 Failed {n} time(s) in {default_branch} in the last 14 days,一眼能看出这是偶发还是长期顽疾。
五、给失败测试配上截图
在 JUnit XML 的 system-out 里加附件标签,路径相对于 $CI_PROJECT_DIR:
xml
<testcase time="1.00" name="TestLogin">
<system-out>[[ATTACHMENT|/path/to/some/file]]</system-out>
</testcase>
同时把截图目录作为产物上传:
yaml
artifacts:
when: always
paths:
- report.xml
- screenshots/
六、写在最后
配测试报告和覆盖率,九成的坑不在测试框架,而在产物路径、体积限制、正则匹配这三处。按上面的顺序排查,基本不需要碰测试代码。
文中用到的完整 .gitlab-ci.yml 模板(Go 与 Node.js 两份)、正则速查表,我整理成了一份清单,需要的同学在评论区留个「覆盖率」,或者到我主页置顶动态里自取,脚本和模板都放那儿了。