TDD 不是先写测试再写代码那么简单
很多人以为 TDD 就是「先把测试用例写完,再去实现功能」。这其实是两件事。先把一整批测试铺好,你并不知道每个测试是不是真的在验证有意义的行为,往往写完实现回头一看,一半测试要么永远是绿的,要么在测实现细节。
真正的 TDD 是一个极小的循环:红、绿、重构,三步之间不停。每一轮只盯着一个具体的行为,测试它、让它通过、再把它收拾干净。它的价值不在于「覆盖率」这个数字,而在于逼你先想清楚:这个东西对外到底承诺什么行为。
测试在这里不是验收报告,而是你设计接口时的第一份使用说明书。
- 红先写一个会失败的测试,失败的原因要清楚。
- 绿用最简单、哪怕笨拙的代码让它通过,不追求优雅。
- 重构测试是绿的之后,再放心地调整结构与命名。
- 小步一轮只解决一个行为,别一次写一摞。
- 快单元测试要跑得飞快,才可能循环上千次。
红:先写一个会失败的测试
循环的起点是一段测试代码,但它必须先失败,而且要因为正确的原因失败。如果你刚写完测试它就通过了,说明你测的东西本来就存在,或者这个测试根本没触达行为。
「红」这一步真正在做的,是用一段调用代码把你想象中的接口写出来。你会立刻感觉到:这个名字顺不顺口、参数合不合理、返回值好不好用。接口的好坏,在被人调用的第一秒就暴露了。
# 以「金额满减计算」为例,先写一个失败的测试(示意)
def test_满额减十元():
# 我们想要的接口长这样:传入金额,返回实付
assert 结账(金额=100) == 90
# 此刻运行:
# 失败 —— 因为「结账」这个函数还不存在。
# 这就是「红」:失败原因清晰,且只因为缺实现。
注意,这一步不要先去设计完整的数据结构,只写一个最朴素的调用。让测试替你把需求说出来。
绿:用最少的代码让它通过
现在写实现,目标只有一个:让这个测试变绿,而且越快越好。不要在这里引入设计模式、不要想着抽象、不要考虑边界情况以外的优雅。
| 心态 | 绿阶段应该做 | 不该做 |
|---|---|---|
| 求快 | 硬编码、直接返回期望值都行 | 提前抽象成通用框架 |
| 求通 | 只覆盖当前这个测试 | 顺手把十个用例都写了 |
| 求简 | 哪怕写法丑陋也没关系 | 担心以后难看不敢动 |
# 为了让上面的测试变绿,写一个「足够笨」的实现:
def 结账(金额):
return 金额 - 10 # 先别管其它规则,绿了再说
# 运行测试:通过。
# 这一刻你拥有的是:一个真实跑通的最小行为。
敢写笨代码,是因为你知道接下来的「重构」会收拾它。怕的是一开始就写得很聪明,结果测试一失败你都不知道是逻辑错了还是抽象错了。
重构:在测试保护下改善结构
测试是绿的,你才拥有了一张安全网。这时才轮到重构:把硬编码换成真正的规则、把长函数拆开、给变量起更准确的名字。每改一处,跑一次测试,它一直是绿的,你就可以放心大胆地改。
- 重构只改内部结构,不改对外行为——测试不变绿的前提就是行为没变。
- 把下一个测试写成新的「红」,推动你把抽象补到位。
- 如果重构之后测试意外变红,说明刚才的改动碰到了行为,回退或修正。
这三步走完,就是一次完整的小循环。然后立刻开始下一个行为:再红、再绿、再重构。TDD 产生的不是测试,而是一套边写边被验证的设计。
怎么挑第一个要测的行为
新手常卡在「不知道从哪个行为开始」。一个实用的办法是:从用户最在意、最容易出错的那条路径开始,而不是从最简单的工具函数开始。
| 候选行为 | 适不适合第一个测 | 原因 |
|---|---|---|
| 核心业务规则 | 适合 | 错了代价大,值得被测试钉住 |
| 输入输出边界 | 适合 | 最容易出问题,回报高 |
| 纯展示格式 | 看情况 | 频繁变动,测试易脆 |
| 样板胶水代码 | 不适合 | 没有复杂逻辑,测了也不牢靠 |
每选一个行为,都问自己一句:如果它错了,用户会不会受影响。会,就值得先写那个红。
哪里适合 TDD,哪里不必硬上
TDD 很强大,但不是所有代码都值得走这个循环。把它用在真正有逻辑判断的地方,回报最高。
# 适合 TDD:纯逻辑、有分支、可独立测的部分
- 计价、折扣、分成规则
- 状态机与流转条件
- 数据清洗与转换
- 权限与校验规则
# 不必硬套 TDD 的部分:
- 单纯的界面布局与样式
- 一次性的脚本迁移
- 与外部接口强耦合、难隔离的胶水层
# 这些地方可以用别的测试方式,不必为了「覆盖率」硬凑循环。
TDD 是给复杂逻辑上保险,不是给每一行代码都写一份成绩单。
把循环变成团队节奏
一个人坚持红-绿-重构已经不容易,团队要做的是让它不依赖某个人的自律。办法是把「测试必须随代码一起进主干」写进流程,而不是靠评审时口头提醒。
- CI 里跑全套单元测试,任何一条红都不允许合并。
- 代码评审时看测试是不是钉住了真实行为,而不是看覆盖率数字。
- 保持单元测试跑得快:几千个测试要在几十秒内跑完,循环才不会让人放弃。
当红-绿-重构成为团队每天的肌肉记忆,你会发现代码不是「写完再补测试」,而是一边被行为验证、一边自然成形。这才是 TDD 真正想给你的东西。