覆盖率与质量门禁
这一课你将能做到
- 给 backend 配置 JaCoCo,本地生成并读懂 HTML 覆盖率报告(行覆盖 vs 分支覆盖)
- 指出「高覆盖仍漏 bug」的典型成因,识别三种虚高覆盖
- 起草一条团队可落地的门禁规约:阈值、排除名单、禁止假绿条款
为什么要学
覆盖率统计「测试执行时碰到了哪些行 / 分支」。Java 侧工具是 JaCoCo,前端是 V8 coverage / Istanbul。人话:覆盖率是地图,标出没人走过的黑区;它不是目的地——assertTrue(true) 也能刷出行覆盖,绿灯数字从来不证明行为正确。
它真正的用法是导航:改完 SubmissionService,打开报告看这次改动的分支有没有被测到;看 AuthService 哪些校验分支从没红过。而**质量门禁(quality gate)**把报告变成「能不能合并」:关键包分支覆盖低于阈值就让 mvn verify 失败。门禁要温和且可执行——一上来全仓 95% 只会逼人写空测、乱加排除。
面试高频:「覆盖率多少合适?」——本课给你一个能背、能自圆其说的答案。
先建立图像
代码行 ────────────────► 被测试跑到了吗? → line %
└─ if / switch 分支 ─► 真假两边都跑过吗? → branch %
高 line + 弱断言 = 假安全感
有意义断言 + 关键模块下限 = 可用门禁
| 指标 | 含义 | 局限 |
|---|
| Line | 行是否执行到 | 不保证断言正确 |
| Branch | 分支两边是否都走到 | 仍可能漏状态组合 |
| 变更覆盖 | PR 改动行是否被测 | 对遗留债最务实 |
金字塔提醒:提覆盖的主战场在单元与薄 MockMvc 层;逼 E2E 刷覆盖又慢又脆。
跟着做
步骤 1:给 backend 挂上 JaCoCo
backend/pom.xml 的 <build><plugins> 里加:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
人话:prepare-agent 在测试运行时插桩统计;report 在 verify 阶段生成 HTML。注意它挂在 verify——这就是为什么 CI 用 mvn -B verify 而不是只 test。
步骤 2:生成并打开报告
cd backend
mvn verify
# 报告位置:
# target/site/jacoco/index.html
浏览器打开 index.html,固定浏览顺序:先看你刚改的包 → 再看 branch 最低的类 → 最后才看全仓均值。均值会安慰人,局部黑洞才害人。点进 SubmissionService:submit 的语言白名单、代码长度、课时校验应该都是绿的菱形(分支两边都走到)或至少有一条已测——红的分支就是盲区。
步骤 3:补一条有意义的分支测,再对比数字
@Test
void submit_课时非法时拒绝() {
assertThatThrownBy(() -> service.submit(3L, 0L, "java", "code"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("课时参数");
}
再跑 mvn verify,看该类 branch % 上升。笔记写一句:数字动了,是因为断言碰到了失败分支——不是因为多写了一个空测。这个因果链是覆盖率课程的心法。
步骤 4:前端 coverage
cd platform
npx vitest run --coverage
首次使用先 npm install -D @vitest/coverage-v8。看终端表格:SubmissionList.tsx 的 error 分支若有 % Stmts 空缺,正是「只测了成功路径」的信号。原则同后端:遗留全仓 100% 不现实,**「你这次改动的逻辑有测」**才是可执行要求。
步骤 5:开启 check 规则,把报告变成门禁(团队共识后)
<execution>
<id>check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.50</minimum>
</limit>
</limits>
</rule>
</rules>
<excludes>
<!-- 排除要有理由,防止名单无限膨胀 -->
<exclude>com/codeforge/entity/**</exclude>
<exclude>com/codeforge/config/**</exclude>
</excludes>
</configuration>
</execution>
阈值第一次落地建议从宽松值起步(如 60%/50%),只锁关键包(service 层),防止「一人擅自把门禁拉到无法合并」。数字必须和团队对齐后再开 check——report 给人导航,check 给团队执法,两者别混为一谈。
步骤 6:识别三种「虚高覆盖」
- 样板命中:大量 getter / equals 被测到,业务方法仍红 → 排除样板或把时间花在规则分支
- 无断言执行:测试调用了方法但只
verify 甚至什么都不查,行变绿、行为没锁 → 补具体 assert
- 只走 true 支:
if (lang 不合法) 的失败支从未进入,line 尚可、branch 露馅 → 补失败路径用例(像步骤 3 那样)
步骤 7:在 PR 描述里写清覆盖变化(习惯)
本 PR:SubmissionService 补课时非法分支;branch +9%
未动:Controller 契约测(已有)
排除:无新增
讲清楚要点
- 执行到 ≠ 断言对:覆盖率不测「对不对」,只测「跑过没」;门禁必须配对「必跑套件 + 有意义断言」才有意义。
- branch 比 line 更能暴露漏测:读报告优先看红色菱形——if 只走了 true 的地方就是历史漏洞候选区。
- 别为数字测样板代码:getter/setter 刷出的百分比没有回归价值,时间要给业务规则与边界。
- 遗留债用变更覆盖偿还:全仓一夜还清不现实;「PR 改动行必须有测」是务实且可执法的下限。
- 提覆盖主战场在单元层:Mockito 快测便宜精准;E2E 刷覆盖既慢又脆,方向就错了。
- 排除名单要写理由:
entity/**、config/** 是合理排除(Lombok 样板与装配代码),但不写理由的排除最终会膨胀成「全排除等于无门禁」。
你可能会懵的地方
-
为什么 100% 行覆盖还有 bug?
并发与时序、真实数据形状、断言太弱、组合状态没覆盖。覆盖率是必要非充分条件——它只能证明「代码被碰过」,不能证明「碰的时候查得严」。
-
mvn test 怎么没有报告?
jacoco:report 挂在 verify 阶段。日常快反馈可以只跑 test,看报告必须 verify——这也解释了 CI 为什么用 mvn -B verify。
-
覆盖率下降是谁的锅?
两种:新代码没带测试,或删了旧测试。PR diff 一眼可辨。把报告当导航而不是指责工具,团队才愿意看它。
-
jacoco 的 check 和 report 有什么不同?
report 只生成给人看的 HTML;check 按规则让构建失败——它才是「门禁」。开 check 的时机在团队对阈值达成共识之后。
-
前端语句覆盖 100% 值得追求吗?
同样可刷。关注交互分支与错误态(error / loading / 空)有没有测,比看总数有用。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 只测 getter/setter | 数字涨、bug 照漏 | 测业务规则与边界 |
| 不断言的调用凑覆盖 | 假绿 | 具体断言返回值 / 状态 / 异常 |
| 强求全仓 100% | 形式主义、排除滥签 | 关键包下限 + 变更覆盖 |
| 用 E2E 刷覆盖 | 慢脆、方向错误 | 单元层补盲区 |
| 删红测保 CI 绿 | 质量假象 | 修代码或修断言 |
卡点急救
verify 后找不到报告目录:确认 report execution 绑定了 verify phase 且没有 <skip> 参数;路径是 target/site/jacoco/index.html。
- 开了
check 全组无法合并:阈值一步到位太狠。降到当前均值附近起步,只锁 service 包,按迭代逐步上调。
- 报告里 Lombok 生成的代码拉低百分比:排除 entity/DTO 包(Lombok 样板),或用
lombok.addLombokGeneratedAnnotation = true 让生成方法带注解再排除。
面试常问
-
覆盖率多少合适?
给框架化答案:绝对数字不重要,分母才重要——「关键业务包(Service 层)分支覆盖 70% 左右,全仓行覆盖 60% 起步,PR 变更行覆盖不下降」。配一句心法:覆盖率是盲区探测器不是目标,虚高的数字比低数字更危险,因为它制造安全感。
-
为什么 line 覆盖率高还是会出线上 bug?
覆盖率只统计「执行到」,不统计「断言严不严」;分支只走一边、组合状态没覆盖、并发时序真实数据形状都是它的盲区。所以团队必须配套「有意义断言」的文化 + branch 维度审查。
-
怎么给遗留项目设计覆盖率门禁?
三步:先全仓 report 摸底;再按模块分层设定起点阈值(避免一刀切卡死交付);最后用变更覆盖要求新代码,让债务只减不增。门禁目标是「不再变坏」,不是「一夜变好」。
自测三问
- line 与 branch coverage 差在哪里?举一个 line 高 branch 低的例子。
- 为什么说覆盖率是地图不是目的地?
- 你给团队的三条门禁里,哪一条最能防止「假绿」?
小结一句话
用 JaCoCo 报告找盲区、用有意义的断言守质量、用变更覆盖还债——覆盖率服务于风险,而不是虚荣心。
延伸