MR 里测试摘要空白、覆盖率不显示?JUnit 与覆盖率配置 9 步排查

本文环境

组件 版本 / 说明
极狐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 两份)、正则速查表,我整理成了一份清单,需要的同学在评论区留个「覆盖率」,或者到我主页置顶动态里自取,脚本和模板都放那儿了。

相关推荐
silvamux_1 小时前
MiniMax API 调用教程:兼容 OpenAI 写法
java·数据库·elasticsearch
zh73141 小时前
sqlforge一个SQL 格式化与方言转换工具
数据库·sql
染指11101 小时前
145.Agent-LangGraph基础概念和安装和简单代码示例
java·服务器·数据库·langchain·agents
太理摆烂哥2 小时前
MySQL表的操作
数据库
lbb 小魔仙2 小时前
时序数据库是什么?2026 年五款工业时序数据库横评与场景解析
数据库·时序数据库
零基础1232 小时前
权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系
java·服务器·数据库
小小龙学IT2 小时前
PostgreSQL 服务端机制与 SQL 实战深度解析:从 MVCC 到查询优化器
数据库·sql·postgresql
l1t2 小时前
DeepSeek总结的DuckDB 从 JSON 字符串到类型化列
数据库·sql·json
Raas1002 小时前
AI网关能做调用审计吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
java·服务器·网络·数据库·gateway·prompt·企业级产品