33、重构与单元测试(一):为重构编写安全网 – 测试策略
重构这件事,说白了就是「不改功能,只改结构」。但问题来了——你怎么知道改完之后功能没变?
靠眼睛看?靠直觉?还是靠「我觉得没问题」?
嗯,我年轻时也这么干过。结果呢?上线当天晚上,线上炸了。从那以后,我养成了一个习惯:重构之前,先织一张安全网。这张网,就是测试。
为什么测试是重构的「安全网」?
你想想看,重构的本质是「在保持外部行为不变的前提下,改善内部结构」。但人的大脑是有极限的,改着改着就容易漏掉某个边界条件。这时候,测试就是你的第二双眼睛。
我个人习惯把测试分成三层:
- 单元测试:验证单个函数/类的行为
- 集成测试:验证模块之间的协作
- 端到端测试:验证整个系统的关键路径
对于重构来说,单元测试是最核心的安全网。为什么?因为重构的粒度通常很小——改一个函数、拆一个类、提取一个接口。这些操作,单元测试都能精准覆盖。
核心原则:重构前,先确保被测代码有足够的测试覆盖率。我个人建议至少 80% 的行覆盖率,关键路径 100%。
测试策略:先写测试,再重构
很多人的习惯是「先改代码,再补测试」。但这样做有个坑——你改完之后,测试可能因为你的改动而「假绿」。说白了,测试本身可能已经失效了。
我建议的顺序是:
- 识别待重构的代码:找到那些「坏味道」——长函数、重复代码、过度耦合等
- 编写测试用例:针对当前行为写测试,覆盖正常路径、边界条件、异常情况
- 运行测试,确保全绿:这是你的基线
- 开始重构:每次改完一小步,就跑一次测试
- 测试持续全绿:说明重构没有破坏功能
小技巧:如果你发现某段代码「很难写测试」,那它往往就是最需要重构的地方。我在项目中遇到过很多次——代码耦合太紧,导致 mock 都 mock 不了。这时候,先拆耦合,再补测试,最后重构。
测试策略的四个维度
在实际项目中,我一般从四个维度来制定测试策略:
| 维度 | 说明 | 我的建议 |
|---|---|---|
| 覆盖度 | 代码行覆盖、分支覆盖、路径覆盖 | 行覆盖 ≥ 80%,分支覆盖 ≥ 70% |
| 粒度 | 单元测试 vs 集成测试 vs E2E | 单元测试占 70%,集成测试 20%,E2E 10% |
| 执行速度 | 测试跑得越快,重构越频繁 | 单元测试 < 100ms/个,全量 < 5分钟 |
| 隔离性 | 测试之间不能互相影响 | 每个测试独立准备、执行、清理 |
你可能会问:「为什么单元测试要占这么多?」
原因很简单——重构的反馈循环越短,你越敢改。单元测试跑一次只要几毫秒,集成测试可能要几秒,E2E 可能要几分钟。你想想看,如果你每次改完都要等 5 分钟才知道有没有改坏,你还敢频繁重构吗?
一个真实的例子
我记得有一次重构一个订单处理模块。原来的代码是一个 300 行的函数,里面混杂着校验、计算、持久化、通知。我第一步不是改代码,而是先写测试。
我写了 12 个测试用例:
- 正常订单:金额正确、状态流转正确
- 边界订单:金额为 0、数量为 0
- 异常订单:库存不足、用户余额不足
- 并发场景:同一订单同时被两个请求处理
测试跑绿之后,我才开始拆函数。每拆出一个函数,就跑一次测试。整个过程大概花了 2 小时,测试一直全绿。最后上线,一次通过。
如果当时没有这张安全网,我估计至少要在线上修 3 个 bug。
避坑指南:我曾经犯过一个错误——测试写得太「脆弱」。比如直接断言某个私有方法的调用次数。结果重构时改了内部实现,测试就红了。但实际上功能没变。所以,测试应该关注「外部行为」,而不是「内部实现」。
测试策略的落地流程
下面这张图是我在团队里推的测试策略流程,你可以参考:
常见测试策略模式
在实际工作中,我总结了几种常见的测试策略模式:
- 黄金 master 模式:先跑一遍全量测试,确保当前代码是「绿的」。然后重构,再跑一遍。如果变红,说明改坏了。
- 测试驱动重构(TDR):先写测试,再重构。测试就是你的「需求文档」。
- 特性开关 + 测试:对于大型重构,可以用特性开关把新旧代码同时部署,通过测试对比输出。
- 契约测试:当重构涉及接口变更时,用契约测试确保上下游的约定不被破坏。
我的习惯:每次重构前,我会先跑一次全量测试,把结果截图保存。重构过程中,每改完一个文件就跑一次测试。如果测试变红,我会立刻回退到上一个「绿点」,而不是继续往下改。这样能保证每次只改一个变量,问题定位非常快。
总结
测试不是重构的负担,而是重构的底气。没有安全网的重构,就像走钢丝不带保险绳——不是不能走,但摔一次就够你受的。
我个人建议:把「先写测试,再重构」变成肌肉记忆。刚开始可能会觉得慢,但等你习惯了,你会发现——重构的速度反而更快了。因为你不怕改坏了。
嗯,这一章就聊到这里。下一章我们聊聊具体的测试编写技巧——如何给遗留代码写测试。