第3章:特征测试(Characterization Tests)
一、导读
1.1 本章学习目标
第3章讲解特征测试——在理解代码之前先记录它的行为。
本章的核心学习目标包括:
- 理解特征测试的目的和方法
- 学会编写特征测试
- 理解特征测试与回归测试的关系
- 掌握处理不确定性的技术
1.2 为什么本章如此重要
面对遗留代码,你往往无法确定什么是"正确"的行为。特征测试不关心对错——它只记录系统实际做了什么。有了这个记录,你就可以安全地重构。
1.3 核心问题引导
特征测试和常规单元测试有什么区别?
如何处理测试中的不确定性(时间、随机数、外部依赖)?
特征测试应该覆盖什么?
特征测试如何帮助重构?
二、核心概念详解
2.1 特征测试的本质
java
// 特征测试:记录行为,不判断对错
@Test
void characterizeProcess() {
LegacyProcessor processor = new LegacyProcessor();
String result = processor.process("test input");
// 不管结果看起来对不对,先记录下来
assertEquals("actual_result_abc123", result);
}
// 有了这个测试,你可以安全地重构
// 如果重构后测试仍然通过,行为没有改变2.2 处理不确定性
java
// 时间依赖
@Test
void characterizeWithTime() {
// 方法1:使用固定时间
// 方法2:记录相对行为
// 方法3:使用接缝替换时间源
}
// 随机数
@Test
void characterizeWithRandom() {
// 使用固定种子
Random random = new Random(42);
// 记录实际输出
}
// 外部依赖
@Test
void characterizeWithExternal() {
// 使用 mock 或 stub
// 记录实际交互
}2.3 特征测试策略
1. 从关键路径开始:先测试最重要的功能
2. 边界值测试:测试极端输入
3. 错误路径测试:测试异常情况
4. 增量添加:逐步增加测试覆盖
5. 保持简单:不追求完美覆盖三、实战案例
3.1 案例一:为复杂计算器编写特征测试
java
public class ComplexCalculator {
public double calculate(String expression) {
// 200 行复杂的解析和计算逻辑
// 没有人完全理解它的行为
}
}
// 特征测试
@Test
void characterizeCalculator() {
ComplexCalculator calc = new ComplexCalculator();
// 记录各种输入的实际输出
assertEquals(3.0, calc.calculate("1 + 2"), 0.001);
assertEquals(6.0, calc.calculate("2 * 3"), 0.001);
assertEquals(-1.0, calc.calculate("1 - 2"), 0.001);
// 边界情况
assertEquals(0.0, calc.calculate("0 * 100"), 0.001);
// 奇怪的行为?记录下来
assertEquals(Double.NaN, calc.calculate("0 / 0"), 0.001);
// 现在可以安全地重构了
}四、本章小结
特征测试:记录系统实际行为,不判断对错。
处理不确定性:固定时间、固定种子、mock 外部依赖。
增量策略:从关键路径开始,逐步增加覆盖。
安全重构:有了特征测试,就可以安全地修改代码。
五、思考与练习
为一个你熟悉的遗留系统编写特征测试。
如何处理测试中的时间依赖?
特征测试应该追求多高的覆盖率?