QA之三 - 变异测试 -- PITest

一、是什么?

  1. 定义
  • 官方:变异测试是一种测试有效性验证技术,通过对源代码注入微小的、故意的错误(“变异体”),验证现有单元测试是否能检测到这些错误 —— 能检测到则测试有效,检测不到则测试存在漏洞。
  • 大白话:把变异测试比作 “黑客攻击你的代码”—— 故意在代码里埋小 bug(比如把 > 改成 <、把 + 改成 -),如果你的单元测试没发现这个 bug,说明测试写得 “假”(覆盖率再高也没用)。
  1. 为什么需要变异测试?(覆盖率的 “致命缺陷”)
    JaCoCo 覆盖率只能告诉你 “代码有没有被执行”,但无法告诉你 “测试能不能发现错误”:
  • ❌ 反例:多签服务中 if (signatureCount > threshold) 这行被执行(覆盖率 100%),但测试只传了 signatureCount=3、threshold=2(只测了 true 分支),如果把 > 改成 <,测试依然通过 —— 覆盖率高但测试无效;
  • ✅ 变异测试:自动把 > 改成 <(生成变异体),如果测试失败,说明测试能检测到这个错误;如果测试通过,说明测试遗漏了关键场景。
  1. PITest 是 Java 生态最主流的变异测试工具。

二、为什么?

  1. 变异测试术语
  • 变异体(Mutant):对源代码的微小修改(故意的错误),如把 signatureCount > threshold 改成 signatureCount < threshold;
    • 条件边界修改:> ↔ <、>= ↔ <=、== ↔ !=,适用if/while 条件判断;
    • 算术操作符修改:+ ↔ -、* ↔ /、% ↔ *,适用数值计算(如手续费计算);
    • 逻辑操作符修改:
    • 赋值操作修改:= ↔ +=、a = b ↔ a = c,如变量赋值(如 threshold = 2 → threshold = 3)
    • 返回值修改:return true ↔ return false,如布尔返回值(如 RPC 调用结果)
  • 杀死(Killed):变异体导致测试失败 → 测试有效,如改完判断逻辑后,测试失败 → 变异体被杀死;
  • 存活(Survived):变异体未导致测试失败 → 测试无效,如改完判断逻辑后,测试通过 → 变异体存活;
  • 未覆盖(Uncovered):变异体所在代码未被测试执行 → 覆盖率问题,如改了未测试的代码行 → 变异体未覆盖;
  • 等价变异体(Equivalent):变异体不改变代码逻辑 → 无法杀死,如把 a = b + 0 改成 a = b → 逻辑等价;
  1. 变异类型
    PITest 的变异操作分为核心内置变异器、进阶变异器、实验性变异器,优先级:核心变异器覆盖 80% 的业务场景,进阶变异器补充边缘场景。

1)核心内置变异器(默认开启)
这是 PITest 最常用的变异操作,覆盖 Java 代码最易出错的场景.

  • CONDITIONALS_BOUNDARY(条件边界修改)
    修改条件判断的边界操作符(如 > ↔ <、>= ↔ <=、== ↔ !=),验证测试是否覆盖条件的所有分支。示例:
原始代码(多签判断) 变异体(故意错误) 测试验证目标
if (signatureCount > threshold) if (signatureCount < threshold) 测试是否覆盖 signatureCount < threshold 分支
if (txId == null) if (txId != null) 测试是否覆盖 txId 非空的场景
while (gasLimit >= 21000) while (gasLimit <= 21000) 测试是否覆盖 gasLimit 低于最小值的场景
  • ARITHMETIC(算术操作符修改)
    修改算术运算符(+ ↔ -、* ↔ /、% ↔ *、++ ↔ --),验证数值计算逻辑的测试有效性。
    示例:
原始代码(手续费计算) 变异体(故意错误) 测试验证目标
fee = gasPrice * gasLimit fee = gasPrice / gasLimit 测试是否校验手续费计算结果的合理性
balance += amount balance -= amount 测试是否覆盖余额扣减的场景
nonce++ nonce-- 测试是否校验交易 nonce 的递增逻辑
  • LOGICAL(逻辑操作符修改)
    修改逻辑运算符(&& ↔ ||、! 新增 / 删除),验证多条件组合判断的测试完整性。
    示例:
原始代码(多条件校验) 变异体(故意错误) 测试验证目标
if (txId != null && !txId.isEmpty()) if (txId != null || !txId.isEmpty()) 测试是否覆盖单条件不满足的场景
return !isValid return isValid 测试是否校验返回值的取反逻辑
while (a || b) while (a && b) 测试是否覆盖多条件组合的边界
  • NCREMENTS(增量操作修改)
    修改增量 / 减量操作(++ ↔ --、+=1 ↔ -=1),是 ARITHMETIC 的子集,专门针对自增 / 自减。示例:
原始代码 变异体 测试验证目标
nonce++ nonce-- 测试是否校验 nonce 递增逻辑
gasUsed += 100 gasUsed -= 100 测试是否校验 gas 消耗计算
  • NEGATE_CONDITIONALS(条件取反)
    对整个条件表达式取反(if (a) → if (!a)),是 CONDITIONALS_BOUNDARY 的补充。示例:
原始代码 变异体 测试验证目标
if (rpcClient.isConnected()) if (!rpcClient.isConnected()) 测试是否覆盖 RPC 断开的场景
return signatureCount >= threshold return !(signatureCount >= threshold) 测试是否校验返回值的取反逻辑

2)进阶变异器(需手动开启,补充场景)
这类变异器默认关闭,需在 PITest 配置中手动指定,覆盖核心场景外的边缘错误类型。

  • RETURN_VALS(返回值修改)
    修改方法的返回值(布尔值 true↔false、数值 0↔1、对象 null↔new实例),验证测试是否校验返回值。示例:
原始代码 变异体 测试验证目标
return true(RPC 调用成功) return false 测试是否校验 RPC 返回 false 的场景
return new TxResult() return null 测试是否处理返回值为 null 的场景
return 21000(默认 gas) return 0 测试是否校验 gas 值为 0 的场景
  • VOID_METHOD_CALLS(空方法调用修改)
    删除 / 替换无返回值方法的调用(如日志打印、事件发送),验证测试是否依赖这些副作用。示例:
原始代码 变异体 测试验证目标
eventEmitter.sendTxEvent(txId) 移除该方法调用 测试是否校验事件发送的副作用
log.info("Tx executed: {}", txId) 移除日志调用 测试是否依赖日志的业务逻辑
  • NULL_RETURNS(空值返回修改)
    作用:将非空返回值改为 null,验证测试是否处理空值异常。

  • PRIMITIVE_WRAPPERS(基本类型包装器修改)
    作用:修改基本类型与包装类型的转换(如 Integer.valueOf(1) → null、int → Integer),验证自动装箱 / 拆箱的测试。

3)实验性变异器(慎用)

  1. 变异测试过程
    image

三、怎么用?
完整的流程:项目代码 → 单元测试 → 集成 PITest → 分析报告 → 优化测试。

  1. 第一步:集成 PITest(Maven 配置)
    PITest 核心依赖是 pitest-maven 插件。

  2. 第二步:编写被测代码与初始测试
    (1)被测代码
    (2)初始测试

  3. 第三步:执行变异测试(PITest 命令)

  4. 第四步:分析 PITest 报告(核心)
    打开 target/pit-reports/index.html,核心内容如下:
    (1)概览页(关键指标)
    表格
    指标 数值(示例) 说明
    Mutation Coverage 50% 50% 的变异体被杀死 → 测试仅能发现一半错误
    Mutants Killed 1 1 个变异体被杀死
    Mutants Survived 1 1 个变异体存活(测试无效)
    Mutants Uncovered 0 无未覆盖的变异体(覆盖率 100%)
    Equivalent Mutants 0 无等价变异体
    (2)类详情页(MultisigService)
    isSignatureEnough 方法:
    变异体:> → < → 存活(测试只测了 true 分支,改完后测试依然通过);
    calculateFee 方法:
    变异体:* → / → 杀死(改完后 100/21000≠2100000,测试失败)。
    (3)变异体详情(存活的变异体)
    PITest 会精准定位存活的变异体:

  5. 第五步:优化测试用例(杀死存活的变异体)

  6. 第六步:重新执行变异测试

四、PITest 高级配置(适配区块链开发场景)

  1. 排除无需变异的代码
  2. 只变异核心业务代码
  3. 配置变异操作(聚焦核心场景)
  4. 加速变异测试(解决慢的问题)
posted @ 2026-03-01 09:56  cac2020  阅读(0)  评论(0)    收藏  举报