先写测试,再写代码——红阶段的正确打开方式
很多人对TDD的第一个疑问是:“没写代码怎么写测试?”其实答案很简单——用测试描述需求。比如我们要实现一个“根据订单金额计算折扣”的函数,需求是:
– 满100元减20元
– 满200元减50元
– 不满100元不打折

这时候不需要写一行业务代码,直接写测试用例(以Python的Pytest为例):
# test_discount.py
def test_calculate_discount():
# 案例1:不满100元,无折扣
assert calculate_discount(99) == 99
# 案例2:刚好满100元,减20
assert calculate_discount(100) == 80
# 案例3:满200元,减50
assert calculate_discount(200) == 150
# 案例4:超过200元,按最高档减
assert calculate_discount(250) == 200
写完测试直接运行,你会看到红色的失败提示——因为calculate_discount
函数根本不存在。这就是TDD的“红阶段”:用测试明确需求边界,让问题暴露在代码编写之前。
这里的关键是:测试要写“输入-输出”的预期,而不是“如何实现”。比如不要在测试里写“函数要用到if-elif”,只需要写“输入100,输出80”——这才是需求的本质。
让测试变绿——最精简的实现技巧
红阶段的目标是“明确问题”,绿阶段的目标则是“用最少的代码解决问题”。注意,是“最少”,不是“最优”——不要提前优化。
针对上面的测试,我们写一个最直白的实现:
# discount.py
def calculate_discount(amount):
if amount >= 200:
return amount - 50
elif amount >= 100:
return amount - 20
else:
return amount
再运行测试,所有断言都会通过——绿色提示出现了!这一步的核心是:只做让测试通过的事,不做多余的事。比如你不需要考虑“未来要加300减80的规则”,也不需要把代码写成“可扩展的架构”——这些都是重构阶段的事。
很多新手会犯的错是“绿阶段就想重构”,结果越写越复杂。记住:绿阶段的代码可以丑,但必须能通过测试。
重构不是优化,是消灭坏味道——绿之后的必要动作
绿阶段完成后,你手上有了“能工作的代码”,但不一定是“好代码”。这时候需要进入重构阶段:在不改变功能的前提下,优化代码的可读性、可维护性。
比如上面的calculate_discount
函数,用了多层if-elif,每次加新规则都要改条件判断——这就是“坏味道”。我们可以用“规则列表”重构:
# discount.py(重构后)
DISCOUNT_RULES = [
(200, 50), # 满200减50
(100, 20), # 满100减20
(0, 0) # 无折扣
]
def calculate_discount(amount):
for threshold, discount in DISCOUNT_RULES:
if amount >= threshold:
return amount - discount
return amount
重构后的代码有两个好处:
1. 扩展性强:加新规则只需要在DISCOUNT_RULES
里加一行,不用改函数逻辑;
2. 可读性高:规则列表一眼就能看懂,比嵌套的if-elif更清晰。
重构时一定要记住:每改一行代码,就跑一次测试。比如上面的规则列表,如果顺序写错(把(100,20)放在(200,50)前面),calculate_discount(200)
会返回180而不是150——这时候测试会立刻变红,帮你发现错误。
TDD的常见坑——你可能犯过的5个错误
TDD看起来简单,但实际操作中容易踩坑。我总结了新手最常犯的5个错误,帮你避坑:
坑1:测试覆盖不足,漏了边界值
比如测试calculate_discount(100)
时,如果你写的是amount > 100
而不是amount >= 100
,测试会失败吗?不会——因为你的测试用例里没有100
这个边界值。一定要测试“刚好达标”的情况,比如100、200这些临界点。
坑2:测试依赖外部资源,不稳定
比如测试一个“读取配置文件的函数”,你用了真实的config.json
文件——这会导致测试“时好时坏”(比如文件被删了、路径变了)。解决方法是用Mock替代真实资源,比如Python的unittest.mock
:
# 测试读取配置文件的函数
from unittest.mock import mock_open, patch
def test_read_config():
mock_data = '{"discount": 20}'
with patch("builtins.open", mock_open(read_data=mock_data)):
config = read_config("config.json")
assert config["discount"] == 20
坑3:把实现细节写进测试
比如你测试calculate_discount
时,断言“函数里用了DISCOUNT_RULES
这个变量”——这就错了!测试应该只关心“输入输出”,不关心“内部怎么实现”。如果重构时把DISCOUNT_RULES
改成DISCOUNT_TABLE
,测试会失败——这不是代码的问题,是测试写得太细。
坑4:忽略边缘 cases
比如测试calculate_discount(-10)
(负数金额)、calculate_discount(0)
(零金额)——这些边缘情况容易被忽略,但往往是bug的来源。解决方法是:把“异常情况”写进测试,比如:
def test_calculate_discount_edge_cases():
# 负数金额,返回原金额(假设需求是“不处理负数”)
assert calculate_discount(-10) == -10
# 零金额,无折扣
assert calculate_discount(0) == 0
坑5:重构时不跑测试
重构的目的是“优化代码”,不是“引入新bug”。但很多人重构后嫌麻烦,不跑测试——结果改坏了功能。记住:重构的每一步都要跑测试,确保功能没变。
TDD工具链——选对工具事半功倍
TDD的落地离不开工具的支持,我整理了不同语言的常用工具链:
语言 | 测试框架 | Mock工具 | 代码覆盖工具 |
---|---|---|---|
Java | JUnit 5 | Mockito | JaCoCo |
Python | Pytest | unittest.mock | coverage.py |
JavaScript | Jest | Jest Mock | Istanbul |
Go | Go Testing | gomock | go test -cover |
以Python为例,coverage.py
可以帮你查看测试覆盖情况:
# 安装coverage.py
pip install coverage
# 运行测试并生成覆盖报告
coverage run -m pytest test_discount.py
# 查看报告
coverage report -m
报告里会显示“哪些代码没被测试覆盖”,比如calculate_discount
函数里的else
分支有没有被测试到——这能帮你补全测试用例。
最后:TDD不是银弹,但能帮你“少踩坑”
很多人问我:“TDD真的能提升代码质量吗?”我的答案是:TDD不是银弹,但能帮你把“问题”暴露在代码编写之前。比如你写代码前先写测试,会被迫想清楚“需求到底是什么”——而不是“先写代码,再补测试”(这时候往往已经踩了坑)。
最后送你一句TDD的名言:“测试是代码的说明书”——好的测试用例,比注释更能说明代码的用途。
你在TDD实践中遇到过什么问题?比如“测试写得太慢”“重构不敢动”?评论区告诉我,我帮你分析~
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/291