实战案例二:遗留代码系统重构(上)—— 理解代码、编写特性测试
说实话,每次接手遗留代码,我心里都会咯噔一下。不是怕改不动,是怕改完之后,系统不工作了,而我自己还不知道。这就像拆一颗炸弹,剪错一根线,后果很严重。
我个人的习惯是:先别急着动手改,先花时间理解它,再给它穿上“防护服”——特性测试。今天这个案例,我们就来聊聊怎么安全地接近遗留代码。
第一步:理解代码——别当“盲人摸象”
你想想看,一段代码跑了五六年,没人敢动,为什么?因为没人真正理解它。我见过一个项目,核心模块有3000行,注释只有三行,还都是错的。
理解遗留代码,我一般分三步走:
- 跑起来,看效果 —— 先把项目在本地跑起来。看看它到底是个什么东西。是Web应用?是批处理?还是后台服务?
- 找入口,理脉络 —— 找到main函数,或者请求入口。顺着它往下走,画出调用链。别想着一次全看懂,先看主干。
- 记笔记,画草图 —— 我个人习惯用纸笔或者思维导图,把关键类、关键方法、数据流向记下来。好记性不如烂笔头。
核心原则: 理解代码不是为了重写它,而是为了知道“改哪里会出问题”。
我曾经接手过一个支付对账系统,代码里有一堆魔法数字和全局变量。我花了整整两天,就干一件事:把每个分支条件都列出来,画成流程图。结果发现,有一个分支逻辑,连原来的开发人员都记不清了。嗯,这就是典型的“遗留代码陷阱”。
第二步:编写特性测试——给代码穿上“防护服”
理解完代码,下一步就是写测试。但注意,这里不是写单元测试,而是写特性测试(Characterization Test)。
什么叫特性测试?说白了,就是把代码当前的行为“固化”下来。不管它是对是错,我们先把它现在的输出记录下来。这样,我们重构的时候,只要输出不变,就说明没改坏。
写特性测试的步骤:
- 选一个函数/方法 —— 从最底层、最独立的函数开始。比如一个工具类方法,或者一个纯计算函数。
- 构造输入 —— 随便给几个典型的输入值。边界值、正常值、异常值,都来一点。
- 记录输出 —— 运行代码,把实际输出记下来。这就是你的“预期结果”。
- 写成断言 —— 把输入和输出写成测试用例。用 assertEquals 之类的断言。
举个例子,假设有一段这样的遗留代码:
public class DiscountCalculator {
public double calculate(double amount, String customerType) {
double discount = 0;
if (customerType.equals("VIP")) {
discount = 0.2;
} else if (customerType.equals("NORMAL")) {
discount = 0.1;
} else {
discount = 0.05;
}
// 这里有个隐藏逻辑:金额超过1000,额外打9折
if (amount > 1000) {
discount = discount * 0.9;
}
return amount * (1 - discount);
}
}
我们不知道这个逻辑对不对,但我们可以先写特性测试:
@Test
public void testCalculate_VIP_Amount500() {
DiscountCalculator calc = new DiscountCalculator();
double result = calc.calculate(500, "VIP");
// 运行一次,得到实际结果,假设是 400.0
assertEquals(400.0, result, 0.001);
}
@Test
public void testCalculate_NORMAL_Amount2000() {
DiscountCalculator calc = new DiscountCalculator();
double result = calc.calculate(2000, "NORMAL");
// 运行一次,得到实际结果,假设是 1620.0
assertEquals(1620.0, result, 0.001);
}
小技巧: 写特性测试时,别纠结“这个逻辑对不对”。先记录,后分析。等测试覆盖全了,再回头审视业务逻辑是否正确。
第三步:构建测试安全网——覆盖关键路径
特性测试不是写一个两个就完事了。你需要构建一张“测试安全网”,把代码的关键路径都覆盖到。
我一般会关注这几个点:
- 条件分支 —— if-else、switch-case,每个分支至少覆盖一次。
- 边界值 —— 比如金额为0、负数、最大值。
- 异常路径 —— 比如传入null、空字符串、非法枚举值。
- 循环/递归 —— 至少覆盖0次、1次、多次的情况。
下面这张图,展示了特性测试在整个重构流程中的位置:
避坑指南:写特性测试时容易犯的错
我刚开始写特性测试时,也踩过不少坑。分享几个典型的:
- 依赖外部资源 —— 测试里连数据库、调远程API。结果测试跑一次要10分钟,根本没法频繁执行。我的建议是:用Mock或者Stub把外部依赖替换掉。
- 测试太“脆弱” —— 断言写得太死,比如检查整个JSON字符串。稍微改个字段顺序就挂了。应该只断言关键输出。
- 只测“快乐路径” —— 只测正常情况,不测异常。结果重构时,异常路径全崩了。记住:遗留代码的bug往往藏在异常路径里。
警告: 千万不要在没写特性测试的情况下,直接重构遗留代码。我曾经有一次,自认为看懂了代码,直接动手改,结果改完发现一个隐藏的边界条件没处理,线上出了P0故障。从那以后,我再也不敢跳过特性测试这一步。
总结一下今天的核心要点
| 步骤 | 核心动作 | 产出物 |
|---|---|---|
| 理解代码 | 跑起来、找入口、画调用链 | 代码结构笔记、调用关系图 |
| 编写特性测试 | 选函数、构造输入、记录输出 | 一组可重复执行的测试用例 |
| 构建安全网 | 覆盖分支、边界、异常路径 | 高覆盖率的测试套件 |
说白了,这一章就干两件事:看懂它,锁住它。看懂代码的意图,锁住代码当前的行为。有了这两步,你才能放心地去动代码。
嗯,今天就先到这里。下一章,我们会在安全网的保护下,开始真正的重构操作。