测试覆盖率策略

让 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 或第二道阈值。适合快速建立基线、减少与遗留代码的摩擦。

切换侧重不会替代团队共识:把「主指标 + 辅指标 + 是否阻塞」写进 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 覆盖

  • 全仓全局阈值:守住基线,允许历史债务存在但不再恶化。
  • 包 / 模块级:对 corebilling 等提高下限或强制分支指标。
  • PR diff / patch 覆盖:新代码必须达到更高下限,与 Codecov patch 等指标对齐并说明 carryforward 行为。

阈值规划器

拖动滑块生成可粘贴进 SKILL 或团队约定的要点列表;切换「行 / 分支 / 并重」会调整文案中的指标名称与相对松紧建议。

阈值(%)

分支通常更难满覆盖:默认比行阈值低若干点;侧重分支时可改为接近 0。

数值需经团队评审;与具体工具链(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 超时的集成测试"

返回技能库 更多技能入口