2.1 红:把需求翻译成会失败的测试


2.1 红:把需求翻译成会失败的测试

本节摘要:红灯阶段决定整个循环的质量——测试切得太厚,绿灯遥遥无期;切得太碎,测试堆成重复噪音。本节给出红灯阶段的完整操作规程:怎么从口语需求里挑出第一条断言、怎么命名、怎么验证失败原因,以及"一条测试只测一件事"的判据。读完你应能把任意一句需求切成厚度合适的失败测试。

「Red」在 TDD 的行话里不是坏消息,是开工令。听到同事说"这条红了",意思是需求的下个切片已经写成了断言,就等实现去兑现。行话能流传,是因为它把一个微妙的立场包装成了颜色:失败不是要消灭的对象,而是前进的路标。本节讲的就是怎么发出开工令——从一团口语需求里,切出下一条厚度合适、指向明确、真的会失败的测试。

先切需求,再写测试

承接第 1 章的密码校验器任务:产品经理的原话是"会员密码得有点强度要求"。这句话至少藏着五个行为:长度下限、必须含数字、必须含大写、必须含特殊字符、强度分级。红灯阶段最容易犯的错是把五个行为塞进一条测试——那样绿灯要一次跨五座山,中途没有任何反馈点。正确切法是挑最薄的第一个切片:哪条行为最小、最能逼出代码骨架,就从它开始。长度检查胜出,因为它是唯一不需要讨论字符集的规则。

# tests/test_password.py from shop.password import rate def test_reject_password_shorter_than_8_chars(): result = rate("aB1x") assert result.valid is False assert "长度不足" in result.reasons

三个细节值得放大。命名写成完整句子:test_reject_password_shorter_than_8_chars 读起来就是需求条目,测试报告因此能当文档看。断言只锁行为不锁结构:我们断言"无效"与"原因里提到长度",不断言内部字段怎么组织——结构是实现的自由。一条测试只含一组相关断言:这里两条断言是同一判断的正反面(判定与理由),不算多事;混进第三条断言别的行为就是越界。

验证失败原因:红灯也要验货

写完测试立刻运行,看到红色不算完——要读失败信息是不是你预期的那种失败。上一节解剖过 pytest 输出的三段结构,这里不重复格式,只强调重演一遍的价值:如果红灯报的是 import 错误,说明骨架还没立,这条红灯验证的是"脚手架缺失"而不是"行为缺失",同样有效,但要心里有数。

> result = rate("aB1x") E ModuleNotFoundError: No module named 'shop.password' tests/test_password.py:2: ModuleNotFoundError

失败原因与预期一致(实现不存在),红灯有效。现在建最空的骨架让失败走到断言层:

# shop/password.py def rate(password: str): ...

再跑一次,红灯从 ModuleNotFoundError 变成 TypeErrorAssertionError——失败点前进到了行为本身,这时的红才是"该实现而未实现"的红。红灯阶段的两段式失败(先环境后行为)很常见,分辨它们是基本功。

图:红-绿-重构循环图

图:红-绿-重构循环图

这张图是全册的发动机结构图,值得贴在显示器边上:三个阶段各占循环的三分之一弧长,暗示时间也要大致均分——红灯阶段花掉一小时、绿灯一分钟,就是切片没切好的信号。

测试名与断言的写法细节

  • 名字即文档test_加会员折扣后小计正确test_discount_1 值钱得多——三个月后测试红了,前者让你秒懂是哪条商业规则被破坏了。
  • 断言优先行为词。"拒绝" "累加" "分级"是行为词;"返回 False" "调用了列表 append"是结构词。行为断言活得长,结构断言死得快。
  • 每个测试自带前提。用例内的对象自己构造自己清理,不依赖别的测试跑没跑、先跑还是后跑。pytest 的 fixture(第 3 章)就是把"前提"写成可复用构件的机制。
  • 别在红灯阶段写辅助函数。测试想保持一眼可读,重复一点没关系,等重构阶段再统一。

⚠️ 反例警告:"测一切"的诱惑在红灯阶段最强。见到有人写出断言返回 JSON 里键顺序的测试,基本可以预言:这测试会在下次无关重构时变红,然后被同事删掉。断言只锁你愿意在未来被通知的事情。

常见红灯事故与处置

事故一:测试永远绿。断言写反(!= 写成 == 后又改错方向)或测了个空操作。处置:写完测试先故意把断言改错验证它真的会红,再改回来。事故二:红灯吓人。一次断言失败带出十页堆栈,多半是异常没被边界捕获。处置:让被测代码在契约边界抛出明确的异常类型,测试用 pytest.raises 接住。事故三:红灯挂太久。半小时过不去,说明切片超过了一轮循环的容量。处置:把这条测试先注释存档,切个更薄的版本先过,再逐步逼近原目标——原测试就是下一轮的红灯。

红灯阶段的自检单

收工前用五问快速自检,答不满就回到对应动作:这条测试的名字是不是一句需求?断言锁的是行为还是结构?失败原因与预期一致吗?一条测试里有没有混进第二个行为?它的前提是不是自带自灭?配合一张命名对照表,红灯阶段的产出质量立刻肉眼可见:

差命名 好命名 差别在哪
test_rate_1 test_reject_password_shorter_than_8_chars 前者是编号,后者是需求句
test_discount test_member_discount_applies_10_percent_off 缺少"谁、什么条件、什么结果"
test_error test_negative_amount_raises_value_error 说清异常类型与触发条件
test_total_ok test_empty_cart_total_is_zero "ok"是废话,边界才是信息

本节要点回顾

  • 「Red」是开工令:失败是待办清单的展示形式,不是事故。
  • 先切最薄的切片:一轮循环只翻一座山,反馈点越密越好。
  • 名字写成需求句,断言只锁行为,前提自带自灭。
  • 红灯要验货:失败原因必须与预期一致,两段式失败要分辨。
  • 红灯挂半小时 = 切片太厚,退回重切,别硬凿。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U