TDD 不是先写测试再写代码那么简单

很多人以为 TDD 就是「先把测试用例写完,再去实现功能」。这其实是两件事。先把一整批测试铺好,你并不知道每个测试是不是真的在验证有意义的行为,往往写完实现回头一看,一半测试要么永远是绿的,要么在测实现细节。

真正的 TDD 是一个极小的循环:红、绿、重构,三步之间不停。每一轮只盯着一个具体的行为,测试它、让它通过、再把它收拾干净。它的价值不在于「覆盖率」这个数字,而在于逼你先想清楚:这个东西对外到底承诺什么行为。

测试在这里不是验收报告,而是你设计接口时的第一份使用说明书。
本文核心知识点
  • 红先写一个会失败的测试,失败的原因要清楚。
  • 绿用最简单、哪怕笨拙的代码让它通过,不追求优雅。
  • 重构测试是绿的之后,再放心地调整结构与命名。
  • 小步一轮只解决一个行为,别一次写一摞。
  • 快单元测试要跑得飞快,才可能循环上千次。
TDD 示意:蓝色渐变背景上,一面发光盾牌对勾、一叠半透明积木方块、一把悬浮扳手,三者由发光弧线首尾相连成闭环循环,扁平科技插画
红-绿-重构不是三条独立任务,而是首尾相接的小循环。

红:先写一个会失败的测试

循环的起点是一段测试代码,但它必须先失败,而且要因为正确的原因失败。如果你刚写完测试它就通过了,说明你测的东西本来就存在,或者这个测试根本没触达行为。

「红」这一步真正在做的,是用一段调用代码把你想象中的接口写出来。你会立刻感觉到:这个名字顺不顺口、参数合不合理、返回值好不好用。接口的好坏,在被人调用的第一秒就暴露了。

# 以「金额满减计算」为例,先写一个失败的测试(示意)
def test_满额减十元():
    # 我们想要的接口长这样:传入金额,返回实付
    assert 结账(金额=100) == 90

# 此刻运行:
#   失败 —— 因为「结账」这个函数还不存在。
# 这就是「红」:失败原因清晰,且只因为缺实现。

注意,这一步不要先去设计完整的数据结构,只写一个最朴素的调用。让测试替你把需求说出来。

绿:用最少的代码让它通过

现在写实现,目标只有一个:让这个测试变绿,而且越快越好。不要在这里引入设计模式、不要想着抽象、不要考虑边界情况以外的优雅。

心态绿阶段应该做不该做
求快硬编码、直接返回期望值都行提前抽象成通用框架
求通只覆盖当前这个测试顺手把十个用例都写了
求简哪怕写法丑陋也没关系担心以后难看不敢动
# 为了让上面的测试变绿,写一个「足够笨」的实现:
def 结账(金额):
    return 金额 - 10   # 先别管其它规则,绿了再说

# 运行测试:通过。
# 这一刻你拥有的是:一个真实跑通的最小行为。

敢写笨代码,是因为你知道接下来的「重构」会收拾它。怕的是一开始就写得很聪明,结果测试一失败你都不知道是逻辑错了还是抽象错了。

重构:在测试保护下改善结构

测试是绿的,你才拥有了一张安全网。这时才轮到重构:把硬编码换成真正的规则、把长函数拆开、给变量起更准确的名字。每改一处,跑一次测试,它一直是绿的,你就可以放心大胆地改。

  • 重构只改内部结构,不改对外行为——测试不变绿的前提就是行为没变。
  • 把下一个测试写成新的「红」,推动你把抽象补到位。
  • 如果重构之后测试意外变红,说明刚才的改动碰到了行为,回退或修正。

这三步走完,就是一次完整的小循环。然后立刻开始下一个行为:再红、再绿、再重构。TDD 产生的不是测试,而是一套边写边被验证的设计。