变异测试

本页提供 Stryker 完整配置文件、变异类型对照表(含代码示例)、变异分数解读标准、如何识别无断言意义的测试,以及只对改动文件运行变异测试的命令配置。

变异测试回答的是「若实现悄悄变坏,测试会不会红」,与覆盖率互补:高覆盖仍大量存活时,往往说明断言弱、只测了 happy path,或变异落在等价语义上。

SKILL 应写明所用工具(如 JavaScript/TypeScript 的 Stryker、Java 的 PIT)、默认变异算子集、超时与并行度、报告路径,以及如何把「等价变异」记入忽略规则或注释,避免团队对噪音免疫。

目标与边界

变异测试不是替代单元测试,而是衡量测试套件对「小错误」的敏感度;不是要求 100% 杀死率(等价变异与过宽算子会抬升成本)。

  • 适合:核心业务模块、近期高变更目录、回归频繁却缺乏行为断言的代码。
  • 谨慎:生成代码、薄封装 DTO、纯数据类(若团队未配置针对性算子)。
  • 与覆盖率:覆盖率高 + 存活多 → 优先补「可观察行为」断言,而非再堆无断言分支覆盖。

记住:指标应驱动改测试与设计,而不是为 dashboard 好看而收紧到不可维护的范围。

推荐流程

  [ 选定范围 ]
  包 / 目录 / diff(增量) + 排除生成物与第三方
        │
        ▼
  [ 配置工具 ]
  算子子集 · 超时 · 并行 · 报告格式 · 等价忽略策略
        │
        ▼
  ┌──────────────┐     PR:增量 / 抽样   nightly:全量或更广
  │  执行变异运行  │──── 失败=构建失败 或 仅上传报告(团队约定)
  └──────────────┘
        │
        ▼
  ┌──────────────┐     分类:测试缺口 / 等价 / 过宽变异 / 真缺陷
  │  存活变异 triage │
  └──────────────┘
        │
        ▼
  [ 产出 ] 新用例或断言 · issue 链到需求 · 更新忽略规则与说明

先让「范围与配置」稳定,再谈杀死率;否则每次报告不可比,团队会放弃看存活列表。

工具与命令要点

在 SKILL 正文中写出可复制的命令模板(含配置文件路径),并注明 Node/JVM 版本与 CI 镜像需与本地一致,避免「仅本地能跑」。

Stryker 完整配置文件(stryker.conf.json):

{
  "$schema": "https://stryker-mutator.io/schemas/stryker-core.json",
  "testRunner": "jest",
  "jest": {
    "projectType": "custom",
    "configFile": "jest.config.ts"
  },
  "mutate": [
    "src/**/*.ts",
    "!src/**/*.test.ts",
    "!src/**/*.spec.ts",
    "!src/**/*.d.ts",
    "!src/generated/**",
    "!src/mocks/**"
  ],
  "mutator": {
    "plugins": ["@stryker-mutator/typescript-checker"],
    "excludedMutations": [
      "StringLiteral",    // 字符串字面量变异(噪音多)
      "ObjectLiteral"     // 对象字面量变异(误报高)
    ]
  },
  "reporters": ["html", "json", "progress"],
  "htmlReporter": {
    "fileName": "reports/mutation/mutation.html"
  },
  "jsonReporter": {
    "fileName": "reports/mutation/mutation.json"
  },
  "thresholds": {
    "high": 80,          // 变异分数 ≥ 80% → 绿色
    "low": 60,           // 变异分数 60-80% → 黄色警告
    "break": 50          // 变异分数 < 50% → CI 失败
  },
  "timeoutMS": 10000,    // 单个变异体超时 10 秒
  "concurrency": 4,      // 并行测试进程数
  "coverageAnalysis": "perTest",  // 只运行覆盖到变异点的测试(性能优化)
  "tempDirName": ".stryker-tmp",
  "cleanTempDir": true
}

变异类型对照表(带代码示例):

类型 1:边界条件变异(ConditionalExpression)
  原始: if (amount >= 100) return discount;
  变异: if (amount > 100) return discount;   // ← >= 改为 >
  意义: 测试必须断言 amount=100 时的行为

类型 2:逻辑运算符变异(LogicalOperator)
  原始: if (isVip && !isBlocked) return premium;
  变异: if (isVip || !isBlocked) return premium;  // ← && 改为 ||
  意义: 测试必须覆盖 isVip=true 但 isBlocked=true 的场景

类型 3:返回值变异(ReturnValue)
  原始: function getDiscount() { return 0.2; }
  变异: function getDiscount() { return 0; }  // ← 返回值改为零值
  意义: 测试必须断言返回值,不能只断言「被调用」

类型 4:语句删除变异(BlockStatement)
  原始: metrics.increment('order.created'); return orderId;
  变异: return orderId;  // ← metrics.increment 被删除
  意义: 测试必须验证 metrics 调用,否则监控代码可随意删除

变异分数解读标准:

变异分数 = 被杀死的变异体 / (总变异体 - 等价变异体) × 100%

> 80%  → 好:测试套件对代码改动高度敏感,适合核心业务模块
60-80% → 中:大多数错误会被捕获,还有改进空间
< 60%  → 差:很多错误可能悄悄溜过测试

具体场景对应:
  85%(好)= 支付模块:所有边界条件和错误路径都有断言
  70%(中)= 用户模块:基本逻辑覆盖了,但错误分支和边界条件断言弱
  45%(差)= 报表模块:测试只断言「函数被调用」,没有断言输出数据是否正确

如何识别「通过了但没有断言意义」的测试:

// 信号:变异体 survived(测试通过但变异存活)
// 说明什么:测试存在,但即使代码被改坏,测试也不会失败

// 示例:ReturnValue 变异存活
// 原始函数:
function calculateTax(amount: number) { return amount * 0.1; }

// 变异版本(Stryker 生成):
function calculateTax(amount: number) { return 0; }  // ← 返回 0

// 测试代码(变异存活的原因):
it('should calculate tax', () => {
  const spy = jest.spyOn(taxService, 'calculateTax');
  checkout.process(100);
  expect(spy).toHaveBeenCalledWith(100);  // ← 只断言参数,不断言返回值!
  // 变异版返回 0 时,此测试仍然通过 → 变异存活
});

// 修复:加上对返回值的断言
it('should calculate 10% tax', () => {
  expect(calculateTax(100)).toBe(10);   // ← 断言实际返回值
  expect(calculateTax(0)).toBe(0);      // ← 边界条件
});

// 只对改动文件运行变异测试(PR 场景):
// package.json:
"mutation:diff": "stryker run --mutate \"$(git diff --name-only origin/main...HEAD | grep -E '\\.(ts|js)$' | grep -v '\\.test\\.' | tr '\\n' ',' | sed 's/,$//').js\"",

// 或使用 Stryker 的 --since 选项(v6+):
// npx stryker run --since origin/main
常见栈与工具(示意,以各工具当前文档为准)
生态 工具 Agent 需记录
JS / TS Stryker stryker.conf、测试运行器、mutate glob、阈值
Java / Kotlin (JVM) PIT 等 目标类、排除、增量、与构建插件集成方式
其他 语言专属 是否支持增量、HTML 报告路径、与 Bazel/Gradle 的交互
  • 为长时间 job 设置明确超时与重试策略,避免占满 CI 队列。
  • 报告保留策略:artifact 保留天数、是否仅主干存档。

范围、性能与 CI

默认对「变更相关」代码跑增量变异;全量放在 nightly 或发布前。对 distgenerated、锁文件与快照目录使用排除规则。

  • PR 门禁:可只要求「无新增高危存活」或「分数不低于基线」,而非每次全量达标。
  • 等价变异:在配置或源码旁注释理由,并定期审计忽略列表,防止永久屏蔽真实风险区。
  • 并行度与分片:大仓库按模块拆 job,便于定位慢在测试还是变异引擎。

存活变异怎么读

每个存活项应能归入下面之一,并在评审中留下可追溯说明。

  • 测试薄弱:可补断言、拆分用例、测错误分支与边界。
  • 等价变异:语义不变 → 配置忽略 + 注释「为何等价」。
  • 算子过宽:对当前代码风格无意义 → 缩小 mutate 范围或关闭子集。
  • 产品/缺陷讨论:变异揭示的行为与需求不一致 → 提 issue,而不是硬改测试去「绿」。

禁止为了数字把断言改成「测实现细节」或过度 mock;优先对齐对外可观察行为与契约。

给 Agent 的输出规范

分析变异报告时,输出应便于人类合并决策,而不是 dump 整份 HTML。

  • 摘要:运行范围、工具版本、总变异数、杀死数、变异分数(若报告提供)。
  • Top 存活:按模块/风险排序,每条含文件、行、变异类型、建议动作(补测 / 等价 / 忽略 / 需求确认)。
  • 与需求的链接:需求 ID 或用户故事,便于评审对照。
  • 后续命令:本地复现的一条命令与配置文件路径。

算子自检与变异分数估算

下方勾选常见变异算子类别,确认团队是否在文档或配置中明确覆盖;右侧用「已杀死 / 总变异体(可扣除等价)」估算变异分数并观察条形示意。

算子覆盖自检

已勾选 0 / 6 类算子(团队文档或配置中已声明覆盖)。

变异分数估算

公式:杀死数 ÷ (总变异体 − 等价变异) × 100%。等价变异填 0 即退化为「杀死 / 总数」。




有效分母 55,变异分数约 76.4%(存活 13)

---
name: mutation-testing-cn
description: Stryker 配置、变异类型识别、分数解读与 CI 增量运行
---

# 步骤 1:安装并初始化 Stryker
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
npx stryker init  # 生成 stryker.conf.json

# 步骤 2:配置关键字段(stryker.conf.json)
mutate: ["src/**/*.ts", "!src/**/*.test.ts", "!src/generated/**"]
thresholds: { high: 80, low: 60, break: 50 }
coverageAnalysis: "perTest"   # 只跑覆盖到变异点的测试(3-5x 加速)
concurrency: 4
timeoutMS: 10000

# 步骤 3:只对改动文件运行(PR 场景)
npx stryker run --since origin/main

# 步骤 4:识别无效断言(mutant survived 的常见原因)
信号: ReturnValue 变异存活 → 测试只断言「被调用」,没断言返回值
信号: ConditionalExpression 存活 → 缺少边界值测试(amount=100 边界)
修复: 把 expect(spy).toHaveBeenCalled() 改为 expect(result).toBe(10)

# 步骤 5:处理等价变异
// 在源码旁注释(Stryker 识别后跳过):
// Stryker disable next-line: ArrowFunction
const noop = () => {};   // 这个函数本身是 noop,变异无意义

# 步骤 6:分数解读标准
> 80%  → 好(适合支付/权限等核心模块)
60-80% → 中(补充边界和错误路径测试)
< 60%  → 差(测试只测了 happy path,断言弱)

# 步骤 7:CI 分层策略
PR:      npx stryker run --since origin/main(只跑改动文件)
nightly: npx stryker run(全量,报告存档 30 天)
门禁:    break: 50(低于 50% 则 CI 失败)

返回技能库 更多技能入口