1.1 先让测试失败:pytest 起步与第一次红


1.1 先让测试失败:pytest 起步与第一次红

本节摘要:TDD 三定律的第一条是——在写出失败的测试之前,不写产品代码。本节从安装 pytest 起步,陪灯塔小组写出整册的第一段失败测试,逐行解剖红灯输出,再用最小实现转绿、顺手做次微重构。读完后你应能独立完成"红、绿、重构"各走一步的最短循环,为第 2 章的完整节律打底。

为什么明明会写实现,还要先写一段注定失败的测试?因为失败信息是需求的另一种写法。"空购物车总额应为零"写在需求文档里是句话,写进测试里是一段机器可执行的断言:它现在红着,说明该做的事还没做完;等它绿了,说明这件事做完了,而且以后有人改坏它会立刻尖叫。把需求翻译成测试,再让测试逼出实现——这就是本节要带你走完的闭环,也是整册教程的发动机。

动手装环境:两条命令的事

承接导读的约定,本册用 Python 3.10 以上版本加 pytest 8.x。创建项目目录,建一个虚拟环境,装 pytest,然后建两个文件夹:shop 放产品代码,tests 放测试。测试与实现分开放不是硬性要求,却能让"测试是需求的镜子"这层关系看得更清楚。

pip install pytest # 装运行器 pytest --version # 确认装上了,输出 8.x

pytest 的用例发现规则简单到近乎固执:默认收集文件名形如 test_*.py 或 *test.py 的文件,再收集其中 test 开头的函数。遵守命名,运行器才能替你把测试找齐。

红一步:空购物车

灯塔小组的需求清单上,最上面一条是"空购物车总额为零"。别小看这条废话需求——它恰好是练手最合适的粒度:一句话、可验证、不牵扯任何设计决策。把它翻译成测试:

# tests/test_cart.py from shop.cart import Cart def test_empty_cart_total_is_zero(): cart = Cart() assert cart.total() == 0

此刻 shop.cart 还不存在。运行 pytest,你会看到一片红——注意,这片红不是事故,是进度:它精确地告诉你,需求清单上有一条已经写成断言但尚未实现。

⚠️ 新手最常犯的错是跳过"亲眼看它红"这一步:测试写完直接写实现,回头一起跑。如果测试本身有笔误(比如断言写反),没有看过红灯的你根本分不清"测试通过"是因为实现对,还是因为测试永远对。红灯是测试的出厂检验。

图:pytest 红灯输出解剖图

图:pytest 红灯输出解剖图

照着图读一遍刚才的输出:E 行说找不到 shop.cart 模块——失败原因正是"实现还没写",与你预期一致,红灯有效。这一步叫"验证失败原因",是 TDD 区别于盲写测试的关键动作。

绿一步:最小的实现

现在写产品代码,但记住"最小"二字:不做任何当前测试没要求的事。空购物车测试只要求两件事——Cart 能被构造,total() 返回零。

# shop/cart.py class Cart: def total(self) -> int: return 0

再跑 pytest,输出变成一个绿点和 1 passed。有人会笑:这也叫购物车?连商品都没有。但对当前这条断言而言,它就是完备的实现——没有一行多余代码,也就没有一处需要辩解的设计。下一轮循环往里加商品时,测试会继续逼着我们把实现长成该有的样子。这便是 TDD 的增量哲学:实现是长出来的,不是画出来的。

重构一步:顺手收拾

绿了之后花几秒看看刚写的代码。这一轮代码量太小,重构空间不大,但有个细节值得立刻处理:total() 返回的是整数,而金额将来会碰到小数。趁测试保护在,把类型约定写清楚:

# shop/cart.py 重构后 from decimal import Decimal class Cart: """购物车:负责条目管理金额汇总,金额单位为元。""" def total(self) -> Decimal: return Decimal("0")

再跑一遍,仍是绿的。注意这次改动改了返回类型,如果项目里已有别的代码依赖 int 返回值,红灯会当场报警——这就是"测试保护下的重构"的含义:你可以放手整理,安全网兜着。重构不改行为,只改结构;判断标准就是测试始终绿。

三定律,先混个脸熟

Kent Beck 把 TDD 的节律压缩成三条铁律,本节你已经无意中执行了前两条,全文引用在此,第 2 章会逐条演练:

  1. 在没有失败的测试之前,不写任何产品代码;
  2. 只写恰好会失败的测试——别一次写十条,也别断言还没需求的功能;
  3. 只写恰好让测试通过的实现——别顺手发挥,超前的能力都是负债。

💡 把三条定律当成编译器报错来理解:没有红灯就写实现,相当于代码里出现非法语句——节律本身在替你拦截"未经需求批准的代码"。

易错点

  • 测试先通过再改实现:先写测试却看到它通过,多半是断言写错了对象,或实现早于测试存在。找出原因,别放过。
  • 失败原因对不上:红灯报的错误与你要实现的能力无关(如 import 失败拼写错误),先修环境与笔误,再进入实现。
  • 绿了就收工:跳过重构的循环会积累"能跑但难读"的债务。哪怕像本节这样只改个类型注解,也算完成了节律。

动手前再送几条排障锦囊。用例没被收集到?先查命名(文件与函数都必须 test_ 打头),再用 pytest --collect-only 看运行器眼里到底有哪些用例——收集阶段的问题用收集清单定位,比盯着执行输出猜快得多。断言浮点数永远差个尾数?回看 1.1 的金额约定:整数分或 Decimal,测试侧配 approx。虚拟环境装了 pytest 却提示找不到命令?多半装进了全局,用 python -m pytest 直接指定解释器最稳。

变式演练:把循环再转一圈

光是听懂不算会,把同样的节律独立再跑一遍:给 Cart 加"能添加商品"的能力,测试先行。第一条红灯写"刚创建的车里没有商品"(它大概率当场就绿——想想为什么:这条断言被现有实现顺带满足了,属于回归测试而非新红灯,按三定律应该再往前探一步,写"添加一件商品后计数为一");第二条红灯逼出 add 方法;第三条红灯处理重复添加同款商品是合并还是并存。三条转完,你对"最小实现"的手感会具体很多。

带走这几句

  • 测试是需求的可执行形态:红灯 = 待办,绿灯 = 完成,循环 = 节拍器。
  • 亲眼看红:没验证过失败原因的测试没有信用。
  • 最小实现:只写当前测试要求的代码,超前能力一律不做。
  • 重构不改行为:测试全程绿,是动刀的前提与证据。
  • 下一节回头看这段节律从哪里来——了解 TDD 的出身,你会更清楚它擅长什么、不擅长什么。

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