测试覆盖率策略
让 Agent 在「数字」与「风险」之间取舍:门禁阈值、排除清单、行/分支/变更 diff 报告与失败时的补测建议;覆盖率是信号,不是刷分目标。
SKILL 应写明采用行、分支还是变更覆盖,以及默认排除生成代码、类型声明与纯配置的规则,避免无意义刷分;对关键模块可要求「新代码 diff 覆盖下限」高于全仓全局阈值,并与 Codecov carryforward 等策略说明一致。
当门禁失败时,Agent 应输出未覆盖分支的业务含义与建议补测类型(单测 / 集成 / E2E),而非仅罗列文件百分比;报告需可在 CI 日志中定位到具体未命中行。
行覆盖 vs 分支覆盖 vs 路径覆盖
行覆盖回答「这行执行过吗」;分支覆盖回答「条件各走向是否都走过」;路径覆盖回答「所有条件组合都验证了吗」。业务逻辑密集、枚举与错误路径多的模块应更重视分支;以数据管道或模板为主的代码可先看行覆盖与关键路径。
// 同一函数,三种覆盖率的差异:
function getDiscount(userLevel: string, amount: number): number {
if (userLevel === 'vip') { // 分支 A
if (amount >= 100) return 0.2; // 分支 B(嵌套)
return 0.1; // 分支 C
}
return 0; // 分支 D
}
// 场景 1:只有一个测试 getDiscount('vip', 150) → 返回 0.2
// 行覆盖率:75%(第 4 行"return 0.1"没走到,第 6 行"return 0"没走到)
// 分支覆盖率:50%(走了 A-true + B-true,A-false 和 B-false 没走)
// 路径覆盖率:25%(共 4 条路径,只覆盖了 A-true→B-true)
// 场景 2:三个测试:('vip',150), ('vip',50), ('normal',0)
// 行覆盖率:100%(所有行都走过)
// 分支覆盖率:100%(A-true/false, B-true/false 都走过)
// 路径覆盖率:75%(3/4 路径,缺 A-true→B-false 即 amount 刚好=100 的边界)
// Jest/Vitest 覆盖率配置(vitest.config.ts / jest.config.ts):
export default defineConfig({
test: {
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'lcov', 'html'],
// 全局阈值(低于则 CI 失败)
thresholds: {
global: {
lines: 80,
branches: 70,
functions: 80,
statements: 80,
},
// 特定模块更高阈值
'src/billing/**': {
lines: 95,
branches: 90,
},
},
// 排除生成代码和类型声明
exclude: [
'src/**/*.d.ts',
'src/**/*.generated.ts',
'src/mocks/**',
'src/**/__tests__/**',
],
},
},
});
有效覆盖 vs 无效覆盖对比:
// ❌ 无效覆盖:测试存在,但断言无意义
it('should call processPayment', () => {
const spy = jest.spyOn(paymentService, 'processPayment');
checkout.submit(orderData);
expect(spy).toHaveBeenCalled(); // ← 只断言「被调用」,不断言结果
// 覆盖率 100% 但测试什么也证明不了
});
// ✅ 有效覆盖:断言可观察行为
it('should return orderId after successful payment', async () => {
jest.spyOn(paymentService, 'processPayment').mockResolvedValue({ success: true });
const result = await checkout.submit(orderData);
expect(result.orderId).toMatch(/^ORD-/); // ← 断言业务结果
expect(result.status).toBe('confirmed'); // ← 断言状态
});
it('should throw PaymentError when card is declined', async () => {
jest.spyOn(paymentService, 'processPayment').mockRejectedValue(
new PaymentError('CARD_DECLINED')
);
await expect(checkout.submit(orderData)).rejects.toThrow('CARD_DECLINED');
// ← 断言错误路径(分支覆盖的价值所在)
});
istanbul ignore 合理使用场景:
// ✅ 合理使用 istanbul ignore
// 1. 调试代码(不应在生产中测试)
/* istanbul ignore next */
if (process.env.DEBUG_MODE) {
console.log('Debug info:', state);
}
// 2. 平台差异代码(当前 CI 只运行在 Linux)
/* istanbul ignore next */
if (process.platform === 'win32') {
return path.win32.join(...parts);
}
// 3. 不可能到达的防御性代码
function exhaustiveCheck(value: never): never {
/* istanbul ignore next */
throw new Error(`Unhandled case: ${value}`);
}
// ❌ 不合理使用(掩盖真实债务)
/* istanbul ignore next */
async function processRefund(orderId: string) {
// 这里有复杂业务逻辑,不应该 ignore
const order = await Order.findById(orderId);
...
}
遗留代码的增量覆盖策略:
# 只要求新增代码(diff)的覆盖率,不强制历史代码
# Codecov 配置(codecov.yml):
coverage:
precision: 2
round: down
range: "70...100"
status:
patch: # 只检查本次 PR 改动的代码
default:
target: 80% # 新增代码必须达到 80%
threshold: 5%
project: # 整体覆盖率不能下降超过 2%
default:
target: auto
threshold: 2%
# 在 CI 中只报新代码覆盖率(nyc / c8)
# package.json scripts:
"test:coverage:diff": "nyc --include='$(git diff --name-only HEAD~1 | grep .ts$ | tr '\\n' ',')' npm test"
当前侧重(影响下方规划器默认建议)
行覆盖优先
门禁以 lines / statements 为主;分支可作为 advisory 或第二道阈值。适合快速建立基线、减少与遗留代码的摩擦。
分支覆盖优先
门禁显式包含 branches;对 if、switch、可选链与异常路径要求更高。适合支付、权限、合规相关包。
两者并重
全仓或包级同时声明行与分支下限;PR 可用 diff 覆盖看「新改动」是否两头达标。文档中写清哪一项阻塞合并、哪一项仅趋势。
切换侧重不会替代团队共识:把「主指标 + 辅指标 + 是否阻塞」写进 SKILL,Agent 生成配置时才能与 Istanbul、c8、JaCoCo、Coverage.py 等工具的字段名对齐。
从采集到门禁
[ 单测 / 集成 / E2E 执行 ]
│
▼
┌─────────────┐ 生成 lcov / cobertura / jacoco.xml
│ 采集工具 │──── 与源码映射、并行 job 合并报告
└─────────────┘
│
▼
┌─────────────┐ 过滤:生成代码、声明文件、配置-only
│ 归一与排除 │──── 版本化 exclude 列表 + Code Review
└─────────────┘
│
▼
┌─────────────┐ 全仓阈值 + 包级 / diff 阈值 + carryforward
│ CI 门禁 │──── 失败 → 未命中行链接 + 补测建议
└─────────────┘
Agent 配置流水线时,应保证「同一 commit 的报告可复现」:固定测试种子(若适用)、清理旧产物、上传单一合并后的覆盖率文件,避免多 job 互相覆盖。
排除规则与审查
- 排除项需代码审阅可解释;禁止大面积
/* istanbul ignore */掩盖债务。 - 全局阈值与包级覆盖可分层配置,防止一刀切拖慢迭代。
- 生成代码目录(protobuf、OpenAPI、路由生成物)优先在工具配置里排除,而非在业务文件里打点忽略。
门禁失败时怎么写
输出结构建议:① 未达标的是行、分支还是 patch;② 涉及的业务场景(用户可见行为、错误码、边界条件);③ 推荐补测层级与最小用例形状(表格驱动 / 契约测试 / E2E 冒烟)。
若仅打印「覆盖率 78%」而无文件与行号,人类与 Agent 都无法高效补测;CI 日志或 Bot 评论应带可点击的 HTML 报告或源码锚点。
分层阈值与 diff 覆盖
- 全仓全局阈值:守住基线,允许历史债务存在但不再恶化。
- 包 / 模块级:对
core、billing等提高下限或强制分支指标。 - PR diff / patch 覆盖:新代码必须达到更高下限,与 Codecov patch 等指标对齐并说明 carryforward 行为。
阈值规划器
拖动滑块生成可粘贴进 SKILL 或团队约定的要点列表;切换「行 / 分支 / 并重」会调整文案中的指标名称与相对松紧建议。
数值需经团队评审;与具体工具链(GitHub Actions、Codecov、Sonar 等)的字段名请在落地时替换为仓库真实配置键名。
---
name: test-coverage-cn
description: 覆盖率配置、行/分支/路径差异、有效覆盖与遗留代码增量策略
---
# 步骤 1:选择覆盖率指标
行覆盖: 快速基线,适合遗留代码(Istanbul lines/statements)
分支覆盖: 业务逻辑密集模块必须(Istanbul branches)
路径覆盖: 高风险模块手动补充边界用例
# 步骤 2:配置 Jest/Vitest 阈值(vitest.config.ts)
thresholds:
global: { lines: 80, branches: 70, functions: 80 }
'src/billing/**': { lines: 95, branches: 90 }
exclude: ['**/*.d.ts', '**/*.generated.ts', 'src/mocks/**']
# 步骤 3:区分有效覆盖和无效覆盖
有效: expect(result.orderId).toMatch(/^ORD-/) ← 断言业务结果
无效: expect(spy).toHaveBeenCalled() ← 只验证调用,无断言结果
# 步骤 4:istanbul ignore 只用于
合理场景: DEBUG_MODE 分支 / 平台差异代码 / exhaustiveCheck
禁止场景: 掩盖复杂业务逻辑的未测试分支
# 步骤 5:遗留代码增量策略
Codecov patch 配置:
status.patch.default.target: 80% # 新代码 80% 覆盖
status.project.default.threshold: 2% # 全局不能下降超过 2%
# 步骤 6:门禁失败时的输出格式
① 指标类型: "branches 覆盖率 65%,低于阈值 70%"
② 具体未覆盖位置: "src/billing/refund.ts:87-92(退款超时分支)"
③ 业务含义: "退款超时时不会触发警报,可能造成用户资金滞留"
④ 补测建议: "在 refund.test.ts 中补一个 mock 超时的集成测试"