5、测试的重要性:重构前的安全保障,编写自测试代码,测试驱动开发(TDD)入门。

说实话,我见过太多人一上来就撸起袖子重构代码。结果呢?改了三行,崩了五个功能。为什么?因为没有测试。

重构的本质是「在不改变外部行为的前提下,改善内部结构」。但你怎么知道行为没变?靠眼睛看?靠感觉?别闹了。你需要一个自动化的安全网——测试。

核心观点:没有测试的重构,不叫重构,叫「瞎改」。

5.1 为什么测试是重构的前提?

我在项目中遇到过最惨的一次教训:一个支付模块,我自认为只是「微调」了一下算法。结果上线后,所有外币订单都算错了汇率。为什么?因为那部分逻辑我根本没测到。

你想想看,重构的本质动作是什么?是「挪动代码」、「提取函数」、「修改继承关系」。这些操作本身就有风险。没有测试,你根本不知道哪里出了问题。

测试的作用说白了就三点:

  • 验证行为不变:重构前后,测试全部通过,说明外部行为没变。
  • 快速反馈:改完代码,跑一遍测试,几秒钟就知道有没有搞砸。
  • 降低心理负担:有测试兜底,你才敢大刀阔斧地改。

警告:不要相信「我改得很小心,不会出问题」。人都会犯错,尤其是改别人写的代码时。测试不是对你不信任,是对代码的尊重。

5.2 编写自测试代码

什么叫「自测试代码」?就是代码自己会测试自己。你不需要手动点来点去,跑一个命令,所有测试自动执行。

我个人习惯,每个模块都配一套单元测试。比如一个简单的订单计算:

// 原始代码
function calculateTotal(price, quantity, discount) {
  return price * quantity * (1 - discount);
}

// 自测试代码(使用 Jest)
describe('calculateTotal', () => {
  test('正常计算', () => {
    expect(calculateTotal(100, 2, 0.1)).toBe(180);
  });

  test('折扣为0', () => {
    expect(calculateTotal(100, 2, 0)).toBe(200);
  });

  test('数量为0', () => {
    expect(calculateTotal(100, 0, 0.1)).toBe(0);
  });
});

你看,这些测试覆盖了正常情况、边界情况。以后我重构这个函数,只要跑一下测试,就知道有没有改坏。

小技巧:写测试时,先写「正常路径」,再写「边界条件」,最后写「异常情况」。我一般按这个顺序来,不容易漏。

5.3 测试驱动开发(TDD)入门

TDD 是什么?简单说就是「先写测试,再写代码」。流程就三步:

  1. :写一个失败的测试
  2. 绿:写最简代码让测试通过
  3. 重构:优化代码,保持测试通过

我第一次接触 TDD 时觉得这太反直觉了。代码都没写,怎么测?但后来我发现,TDD 最大的好处是——它逼你想清楚「到底要做什么」。

举个例子,假设我们要实现一个「判断闰年」的函数:

// 第一步:写测试(红)
describe('isLeapYear', () => {
  test('能被400整除的是闰年', () => {
    expect(isLeapYear(2000)).toBe(true);
  });

  test('能被100整除但不能被400整除的不是闰年', () => {
    expect(isLeapYear(1900)).toBe(false);
  });

  test('能被4整除但不能被100整除的是闰年', () => {
    expect(isLeapYear(2024)).toBe(true);
  });

  test('不能被4整除的不是闰年', () => {
    expect(isLeapYear(2023)).toBe(false);
  });
});

// 第二步:写最简代码(绿)
function isLeapYear(year) {
  if (year % 400 === 0) return true;
  if (year % 100 === 0) return false;
  return year % 4 === 0;
}

// 第三步:重构(这里代码已经够简洁,不需要重构)

为什么会这样?因为测试先定义了「正确行为」,你只需要让代码符合这个行为就行。重构时,测试就是你的安全网。

关键点:TDD 不是测试方法,是设计方法。它让你写出更可测试、更模块化的代码。

5.4 测试金字塔与策略

在实际项目中,测试不是越多越好。我见过有人写了上千个单元测试,但集成测试一个都没有。结果呢?模块单独跑没问题,一合起来就崩。

测试应该分层:

层级 速度 数量 作用
单元测试 快(毫秒级) 验证单个函数/模块
集成测试 中(秒级) 验证模块间协作
端到端测试 慢(分钟级) 验证完整业务流程

我个人习惯:单元测试占 70%,集成测试占 20%,端到端测试占 10%。这个比例不是绝对的,但能保证大部分问题在早期被发现。

避坑指南:我曾经犯过一个错误——过度依赖端到端测试。每次跑测试要等 20 分钟,开发效率极低。后来我把大部分逻辑下沉到单元测试,反馈速度从分钟级降到了秒级。

5.5 测试与重构的协作流程

下面这张图展示了测试在重构中的核心位置:

测试与重构协作流程 现有代码 编写测试 执行重构 运行测试 修复/回滚 测试失败 → 回滚 测试通过 → 继续重构

流程很简单:先有代码,再写测试,然后重构。重构后跑测试,通过就继续,不通过就回滚或修复。这个循环保证了每一步都是安全的。

5.6 常见误区与建议

嗯,这里要注意几个常见的坑:

  • 不要追求 100% 覆盖率:我见过有人为了覆盖率数字好看,写一堆没意义的测试。覆盖率 80% 的优质测试,远好过 100% 的垃圾测试。
  • 测试代码也是代码:它也需要重构、需要维护。别把测试写成「一次性用品」。
  • 先写核心逻辑的测试:如果你时间有限,优先测试那些「改错了会出大事」的代码。

我的建议:刚开始用 TDD 时,别贪多。选一个小模块,完整走一遍「红-绿-重构」流程。感受一下那种「测试通过」的踏实感。你会爱上它的。

测试不是负担,是投资。你花在测试上的每一分钟,都会在重构时十倍百倍地还回来。

公众号:蓝海资料掘金营,微信deep3321