QA之三 - 变异测试 -- PITest
一、是什么?
- 定义
- 官方:变异测试是一种测试有效性验证技术,通过对源代码注入微小的、故意的错误(“变异体”),验证现有单元测试是否能检测到这些错误 —— 能检测到则测试有效,检测不到则测试存在漏洞。
- 大白话:把变异测试比作 “黑客攻击你的代码”—— 故意在代码里埋小 bug(比如把 > 改成 <、把 + 改成 -),如果你的单元测试没发现这个 bug,说明测试写得 “假”(覆盖率再高也没用)。
- 为什么需要变异测试?(覆盖率的 “致命缺陷”)
JaCoCo 覆盖率只能告诉你 “代码有没有被执行”,但无法告诉你 “测试能不能发现错误”:
- ❌ 反例:多签服务中 if (signatureCount > threshold) 这行被执行(覆盖率 100%),但测试只传了 signatureCount=3、threshold=2(只测了 true 分支),如果把 > 改成 <,测试依然通过 —— 覆盖率高但测试无效;
- ✅ 变异测试:自动把 > 改成 <(生成变异体),如果测试失败,说明测试能检测到这个错误;如果测试通过,说明测试遗漏了关键场景。
- PITest 是 Java 生态最主流的变异测试工具。
二、为什么?
- 变异测试术语
- 变异体(Mutant):对源代码的微小修改(故意的错误),如把 signatureCount > threshold 改成 signatureCount < threshold;
- 杀死(Killed):变异体导致测试失败 → 测试有效,如改完判断逻辑后,测试失败 → 变异体被杀死;
- 存活(Survived):变异体未导致测试失败 → 测试无效,如改完判断逻辑后,测试通过 → 变异体存活;
- 未覆盖(Uncovered):变异体所在代码未被测试执行 → 覆盖率问题,如改了未测试的代码行 → 变异体未覆盖;
- 等价变异体(Equivalent):变异体不改变代码逻辑 → 无法杀死,如把 a = b + 0 改成 a = b → 逻辑等价;
- 变异类型
PITest 的变异操作分为核心内置变异器、进阶变异器、实验性变异器,优先级:核心变异器覆盖 80% 的业务场景,进阶变异器补充边缘场景。
pitest不同版本的编译器可能不同,在执行时日志里会打印当前版本可用的编译器:
点击查看代码
version 1.20.7
[INFO] Available mutators : EXPERIMENTAL_ARGUMENT_PROPAGATION,FALSE_RETURNS,TRUE_RETURNS,CONDITIONALS_BOUNDARY,CONSTRUCTOR_CALLS,EMPTY_RETURNS,INCREMENTS,INLINE_CONSTS,INV
ERT_NEGS,MATH,NEGATE_CONDITIONALS,NON_VOID_METHOD_CALLS,NULL_RETURNS,PRIMITIVE_RETURNS,REMOVE_CONDITIONALS_EQUAL_IF,REMOVE_CONDITIONALS_EQUAL_ELSE,REMOVE_CONDITIONALS_ORDE
R_IF,REMOVE_CONDITIONALS_ORDER_ELSE,VOID_METHOD_CALLS,EXPERIMENTAL_BIG_DECIMAL,EXPERIMENTAL_BIG_INTEGER,EXPERIMENTAL_MEMBER_VARIABLE,EXPERIMENTAL_NAKED_RECEIVER,REMOVE_INCREMENTS,EXPERIMENTAL_SWITCH
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 低于最小值的场景 |
- MATH(算术操作符修改)
修改算术运算符(+ ↔ -、* ↔ /、% ↔ *、++ ↔ --),验证数值计算逻辑的测试有效性。
示例:
| 原始代码(手续费计算) | 变异体(故意错误) | 测试验证目标 |
|---|---|---|
| fee = gasPrice * gasLimit | fee = gasPrice / gasLimit | 测试是否校验手续费计算结果的合理性 |
| balance += amount | balance -= amount | 测试是否覆盖余额扣减的场景 |
| nonce++ | nonce-- | 测试是否校验交易 nonce 的递增逻辑 |
- TRUE_RETURNS、FALSE_RETURNS(布尔返回值修改)
修改逻辑运算符(&& ↔ ||、! 新增 / 删除),验证多条件组合判断的测试完整性。
示例:
| 原始代码(多条件校验) | 变异体(故意错误) | 测试验证目标 |
|---|---|---|
| if (txId != null && !txId.isEmpty()) | if (txId != null || !txId.isEmpty()) |
测试是否覆盖单条件不满足的场景 |
| return !isValid | return isValid | 测试是否校验返回值的取反逻辑 |
while (a || b) |
while (a && b) | 测试是否覆盖多条件组合的边界 |
- INCREMENTS(增量操作修改)
修改增量 / 减量操作(++ ↔ --、+=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) | 测试是否校验返回值的取反逻辑 |
- NULL_RETURNS(空值返回修改)
作用:将非空返回值改为 null,验证测试是否处理空值异常。示例:
| 原始代码 | 变异体 | 测试验证目标 |
|---|---|---|
| return signatureUtils.sign(txId) | return null | 测试是否处理签名为 null 的场景 |
- PRIMITIVE_WRAPPERS(基本类型包装器修改)
作用:修改基本类型与包装类型的转换(如 Integer.valueOf(1) → null、int → Integer),验证自动装箱 / 拆箱的测试。示例:
| 原始代码 | 变异体 | 测试验证目标 |
|---|---|---|
| Integer gasLimit = 21000 | Integer gasLimit = null | 测试是否处理包装类型 null 的场景 |
另外还有:
- EMPTY_RETURNS:集合 / 字符串返回值改空;
2)进阶变异器(需手动开启,补充场景)
这类变异器默认关闭,需在 PITest 配置中手动指定,覆盖核心场景外的边缘错误类型。
- INVERT_NEGS:取反操作反转,新增 / 删除取反符号(!a→a、a→!a),签名有效性 !isValid → 改成 isValid,验证取反逻辑测试;
- INLINE_CONSTS:常量内联修改,修改常量值(如 final int THRESHOLD=2 → 改成 3),多签门限常量 THRESHOLD=2 → 改成 3,验证常量依赖的测试
- VOID_METHOD_CALLS:空方法调用移除,删除无返回值方法的调用(日志、事件发送、RPC 通知),移除 eventEmitter.sendTxEvent(txId),验证事件发送的副作用测试
- NON_VOID_METHOD_CALLS:非空方法调用替换,替换有返回值方法的调用结果(如 rpcClient.getGasPrice() → 返回固定值),RPC 获取 gasPrice 改成固定值 0,验证依赖外部数据的测试
- CONSTRUCTOR_CALLS:构造方法调用修改,替换 / 移除构造方法调用(如 new TxResult() → null),交易结果对象 new TxResult() → 改成 null,验证对象创建的测试
- REMOVE_INCREMENTS:增量操作移除,删除自增 / 自减操作(nonce++ → 无操作),移除 nonce++,验证 nonce 未递增时的测试
- REMOVE_CONDITIONALS_EQUAL_IF:等值条件 IF 分支移除,移除 if (a == b) 中的 IF 分支(直接执行 else),移除 if (txId == targetTxId) 的 IF 分支,验证条件分支的测试
- REMOVE_CONDITIONALS_EQUAL_ELSE:等值条件 ELSE 分支移除,移除 if (a == b) 中的 ELSE 分支(直接执行 if),移除 if (signatureCount == threshold) 的 ELSE 分支,验证边界分支测试
- REMOVE_CONDITIONALS_ORDER_IF:顺序条件 IF 分支移除 移除 if (a > b) 等顺序条件的 IF 分支,移除 if (gasLimit > 21000) 的 IF 分支,验证顺序条件测试
- REMOVE_CONDITIONALS_ORDER_ELSE:顺序条件 ELSE 分支移除,移除 if (a > b) 等顺序条件的 ELSE 分支,移除 if (fee > maxFee) 的 ELSE 分支,验证手续费上限测试
3)实验性变异器(慎用)
这类变异器处于实验阶段,可能不稳定,仅适用于特定场景(如 lambda、注解、并发代码)。
- EXPERIMENTAL_ARGUMENT_PROPAGATION:参数传播修改,修改方法参数的传递(如 method(a) → method(b));checkSignature(null),仅测试参数校验逻辑时开启
- EXPERIMENTAL_BIG_DECIMAL:BigDecimal 操作修改,修改 BigDecimal 的算术操作(add→subtract、multiply→divide,手续费(BigDecimal)fee.add(priorityFee) → 改成 subtract;
- EXPERIMENTAL_BIG_INTEGER:BigInteger 操作修改,修改 BigInteger 的算术 / 比较操作,区块高度(BigInteger)height.compareTo(maxHeight) → 反向;
- EXPERIMENTAL_MEMBER_VARIABLE:成员变量修改,修改类成员变量的值(如 this.threshold=2 → 3),多签服务成员变量 threshold 改成 3,验证成员变量依赖测试
- EXPERIMENTAL_NAKED_RECEIVER:裸接收者修改,修改方法调用的接收者(如 a.method() → b.method()),rpcClient.sendTx() → 改成 mockClient.sendTx();
- EXPERIMENTAL_SWITCH:Switch 分支修改,修改 switch 分支的执行逻辑(如 case 1 → case 2)
- 变异测试过程

三、怎么用?
完整的流程:项目代码 → 单元测试 → 集成 PITest → 分析报告 → 优化测试。
- 第一步:集成 PITest(Maven 配置)
PITest 核心依赖是 pitest-maven 插件。
点击查看代码
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Test3</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一指定项目编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一指定项目报告编码(可选,避免报告乱码) -->
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- JUnit 5 核心依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- 参数化测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito 核心依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito 适配 JUnit 5 的扩展 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- 可选:静态方法Mock依赖(如需Mock静态工具类) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- pitest适配JUnit5依赖 必要 -->
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 执行快速、独立的单元测试(失败会直接中断构建)-->
<!--<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>-->
<!-- 专门用于执行集成测试(Integration Tests) -->
<!--<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>-->
<!-- 1. JaCoCo 核心插件(生成覆盖率报告) -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version> <!-- 最新稳定版 -->
<configuration>
<excludes>
<!-- 排除配置类 -->
<exclude>com/multisig/config/**/*</exclude>
<!-- 排除工具类常量 -->
<exclude>com/multisig/constants/**/*</exclude>
<!-- 排除测试专用代码 -->
<exclude>**/*Test*.class</exclude>
</excludes>
</configuration>
<executions>
<!-- 执行测试时收集覆盖率数据 -->
<execution>
<id>jacoco-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- 测试完成后生成覆盖率报告 -->
<execution>
<id>jacoco-report</id>
<phase>test</phase> <!-- 绑定到 mvn test 阶段 -->
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- 可选:强制执行覆盖率规则(如行覆盖率≥80%) -->
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<!-- 对所有代码的行覆盖率要求 ≥80% -->
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.8</minimum> <!-- 80% 行覆盖 -->
</limit>
<!-- 分支覆盖率 ≥70% -->
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.7</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<!-- 2. 确保 Maven Surefire 插件适配 JUnit 5 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<includes>
<include>**/*Test.java</include> <!-- 执行所有测试类 -->
</includes>
</configuration>
</plugin>
<!-- PITest 核心插件(变异测试) -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.20.7</version> <!-- 最新稳定版 -->
<configuration>
<!-- 1. 适配 JUnit 5(必须) -->
<testPlugin>junit5</testPlugin> <!-- 使用 JUnit 5 时必须指定 -->
<!-- 2. 配置测试范围(可选) -->
<targetClasses>
<param>org.example.*</param> <!-- 只测试多签服务包 -->
</targetClasses>
<targetTests>
<param>org.example.*Test</param> <!-- 只执行多签测试类 -->
</targetTests>
<!-- 3. 排除无需变异的代码(可选,如配置类) -->
<excludedClasses>
<param>org.example.config.*</param>
<param>org.example.constants.*</param>
</excludedClasses>
<!-- 4. 配置变异操作(可选,默认全开启) -->
<mutators>
<mutator>CONDITIONALS_BOUNDARY</mutator> <!-- 条件边界修改 -->
<mutator>MATH</mutator> <!-- 算术操作符修改 -->
<mutator>NEGATE_CONDITIONALS</mutator> <!-- 逻辑操作符修改 -->
</mutators>
<!-- 5.性能优化(JDK 11 多线程加速) -->
<threads>4</threads> <!-- 按CPU核心数配置,如4/8 -->
<timeoutFactor>1.5</timeoutFactor> <!-- 超时因子(缩短超时) -->
<skipFailingTests>true</skipFailingTests> <!-- 跳过失败的测试 -->
<!-- 6.报告配置(生成HTML+XML) -->
<outputFormats>
<format>HTML</format>
<format>XML</format>
</outputFormats>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 第二步:编写被测代码与初始测试
(1)被测代码
点击查看代码
package org.example;
public class MultisigService {
private int threshold = 2; // 多签门限
// 核心方法:判断签名数是否满足门限
public boolean isSignatureEnough(int signatureCount) {
// 核心判断(易出错点)
if (signatureCount > threshold) {
return true;
} else {
return false;
}
}
// 核心方法:计算交易手续费(算术操作)
public long calculateFee(long gasPrice, long gasLimit) {
return gasPrice * gasLimit; // 算术操作(易出错)
}
}
(2)初始测试代码
点击查看代码
package org.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MultisigServiceTest {
private MultisigService service = new MultisigService();
// 只测了 signatureCount > threshold 的场景(true分支)
@Test
void testIsSignatureEnough_Enough() {
assertTrue(service.isSignatureEnough(3));
}
// 只测了正常算术计算,未测边界
@Test
void testCalculateFee() {
assertEquals(2100000, service.calculateFee(100, 21000));
}
}
-
第三步:执行变异测试(PITest 命令)
执行变异测试并生成报告mvn clean test org.pitest:pitest-maven:mutationCoverage -
第四步:分析 PITest 报告(核心)
打开 target/pit-reports/index.html,核心内容如下:
(1)概览页(关键指标)

- Line Coverage:变异行覆盖率,你的单元测试实际执行到的、且被 PITest 注入了变异体的代码行数,占所有生成了变异体的代码总行数的比例。
PITest Line Coverage = (被测试执行的变异行数量) / (总变异行数量) × 100% - Mutation Coverage:代表变异测试覆盖率,你的单元测试能够 “处理”(杀死 / 检测到)或 “执行到” 的变异体数量,占总变异体数量的比例
Mutation Coverage = (总变异体数量 - 未覆盖的变异体数量) / (总变异体数量 - 等价变异体数量) × 100% - Test Strength:测试强度 是 PITest 1.20+ 版本新增的核心指标(部分旧版本叫 Mutation Score/Mutation Coverage),本质是:衡量你的单元测试能有效发现代码错误的能力强弱,数值越高,说明测试越能识别代码中的微小错误(变异体),测试质量越高。
Test Strength = (被杀死的变异体数量) / (总变异体数量 - 等价变异体数量) × 100%
(2)变异体详情(存活的变异体)

PITest 会精准定位存活的变异体:
浅绿色的代码行表示已经被测试用例覆盖;
深绿色的代码行表示已经被变异测试覆盖;
浅粉红色的代码行表示没有被测试用例覆盖;
深粉红色的代码行表示还没有被变异测试覆盖;
- Mutations:指 PITest 为当前类 / 方法生成的所有变异体数量(包含所有状态:Killed / 存活 / 未覆盖 / 等价),是评估 “当前代码被注入多少个故意错误” 的基础指标。Covering tests表示所覆盖的测试用例。
- Active mutators:指 本次变异测试中,PITest 为当前类 / 方法实际启用的变异器类型列表
- Tests examined:指 PITest 为了验证当前类 / 方法的每个变异体,实际执行的单元测试用例数量和执行时间,反映 “多少个测试用例参与了当前代码的变异验证”。
- 第五步:优化测试用例(杀死存活的变异体)
点击查看代码
// 补充测试:覆盖 signatureCount < threshold 的场景(false分支)
@Test
void testIsSignatureEnough_NotEnough() {
assertFalse(service.isSignatureEnough(1)); // 测false分支
}
- 第六步:重新执行变异测试
mvn clean test org.pitest:pitest-maven:mutationCoverage
四、PITest 高级配置(适配区块链开发场景)
- 排除无需变异的代码
点击查看代码
<configuration>
<excludedClasses>
<param>com.multisig.config.*</param> <!-- 排除配置类 -->
<param>com.multisig.constants.*</param> <!-- 排除常量类 -->
<param>com.multisig.utils.*</param> <!-- 排除工具类 -->
</excludedClasses>
</configuration>
- 只变异核心业务代码
点击查看代码
<configuration>
<targetClasses>
<param>com.multisig.MultisigService</param>
<param>com.multisig.TimelockService</param>
</targetClasses>
</configuration>
- 配置变异操作(聚焦核心场景)
点击查看代码
<configuration>
<mutators>
<mutator>CONDITIONALS_BOUNDARY</mutator> <!-- 条件边界(门限判断) -->
<mutator>ARITHMETIC</mutator> <!-- 算术操作(手续费计算) -->
<mutator>RETURN_VALS</mutator> <!-- 返回值修改(RPC结果) -->
</mutators>
</configuration>
- 加速变异测试(解决慢的问题)
点击查看代码
<configuration>
<threads>4</threads> <!-- 多线程执行(CPU核心数) -->
<timeoutFactor>1.5</timeoutFactor> <!-- 超时因子(缩短超时时间) -->
<skipFailingTests>true</skipFailingTests> <!-- 跳过失败的测试 -->
</configuration>
学习技术不是用来写HelloWorld和Demo的,而是要用来解决线上系统的真实问题的.

浙公网安备 33010602011771号