第11章 测试驱动开发

第11章 测试驱动开发

学习目标

  • 能说出 TDD 三阶段每一步要交付什么,并把验收测试、单元测试与 fake、mock 替身放到正确的测试层级。
  • 能为一个新函数先写失败测试、再写最小实现、最后重构,让测试全程保持绿色。
  • 能回答:为什么把复杂搭建代码留在 doctest 里,反而会让文档失效?

为什么先写测试再写代码

想象一条流水线:如果等整批产品做完才检查,发现问题时已经造了一仓库次品,只能整批报废。把检查前移到每一道工序之后,次品当场被发现、当场修掉,仓库里就不会堆积返工。

写代码也是同一个道理。先把“什么算成功”写成一段能自动判定的检查,再动手写代码,就等于给每一行代码配上当场把关。检查先失败、再被代码满足,你能确定每一行代码都是为了通过某条检查而存在,没有一行是“也许有用”的猜测。

没有这层当场把关,代码“在我电脑上能跑”只能证明它没在你电脑上崩溃过,不能证明它对别人、对没料到的输入、对将来还会对。本章把这些“当场把关”组织成可重复的流程。

先做预测

先预测:一段代码“在作者机器上跑通了”,能不能就算它可维护?不能。要可维护,还得钉死解释器和依赖版本、写清楚喂进去的输入和边界、留下失败时该报什么错,并给出能在干净机器上重放结果的命令。本章把这份契约放进每个测试循环里反复兑现。

原书骨架与现代迁移

官方章节围绕 TDD 原则、验收测试与单元测试、标准测试工具、Fake 与 Mock、文档驱动开发五条主线展开。Tarek Ziade 写作时 Python 还停在 2.5、敏捷工具生态也还在早期,所以本章只继承它解释“为什么这样测”的骨架;具体的命令与安全默认则改按当下的 Python、标准库和 PyPA 文档来迁移——不会把 EasyInstall、distutils 安装命令或老 CI 平台直接当成新项目的默认。

读的时候沿一条线追:输入是什么、约定了什么协议、状态怎么变、输出是什么、留下了什么证据。这样才不会只背 API 名字,却说不清一个测试到底证明了什么。

把“先失败再实现”的循环用代码钉下来,下面左右两栏看同一段测试逻辑的两种视角:

import unittest
 
class ParserTests(unittest.TestCase):
    def test_rejects_empty_payload(self):
        with self.assertRaises(ParseError):
            parse(b"")

机制一:责任与数据流

先写会失败的行为示例,再写最小实现并重构——这种循环驱动的是接口与反馈,不追求每行实现都先配一个脆弱断言。其中从用户边界证明能力。与之配套的则快速验证局部规则。两者之间还需要集成测试覆盖数据库、文件与网络适配。先写出公共行为和失败类型,再选语法或工具,这样即使实现从原书工具迁到当前生态,调用者仍能依据同一份契约判断结果。

抽象不会消除成本,只会把它挪到别处:包装、生成器、构建系统、CI 或缓存都得讲清耗了什么资源、按什么顺序、异常怎么传播。

替身怎么选,先看这种简化但能跑通的实现,下面左右两栏看同一段替身逻辑的两种视角:

class FakeStore:
    def __init__(self): self.saved = []
    def save(self, entries):
        self.saved.extend(entries)
        return len(entries)

机制二:失败与边界

unittest、doctest 和测试发现提供标准反馈环;原书的 nose 与早期 py.test 展示替代入口,现代迁移时要统一 fixture、参数化和失败报告。替身这边,fake 给出简化但能工作的实现,mock 验证特定交互;优先断言可见结果,只有协议本身重要时才锁定调用次数和顺序。边界实验至少包含正常、空输入、上限附近、依赖失败和重复执行;涉及网络、并发或外部制品时,再加超时、取消、部分完成与摘要校验。

老工具不是没有价值。正确的迁移是先抽出声明式配置、隔离、持续反馈和可回滚发布这些不变量,再用还在维护的接口重写;只机械替换命令、却留下隐式环境和不可追踪的副作用,是错误做法。

把文档示例变成可执行证据,下面左右两栏看同一段文档驱动逻辑的两种视角:

def add(a, b):
    """Return a sum.
 
    >>> add(2, 3)
    5
    """
    return a + b

机制三:证据闭环

这一节负责把验收收束起来。故事和 doctest 把可读示例变成可执行证据,适合稳定的小接口;复杂环境则应移到测试模块,免得文档被一堆搭建代码淹没。要让另一台干净机器重放出同一结论,就得保存解释器与依赖锁定、输入、命令、退出状态、关键输出和制品摘要。

先预测:把上面的循环在一个可点开的面板里走一遍,红灯、绿灯、重构三步分别会停在哪个证据上?

测试驱动开发
TDD原则、验收与单元测试、测试工具、Fake与Mock、文档驱动TDDTDD原则测试层级验收测试与单元测试测试工具标准测试工具Fake与MockFake与Mock文档驱动文档驱动开发
TDD原则

先写会失败的行为示例,再写最小实现并重构。RED→GREEN→REFACTOR三阶段循环。

实战验收清单

  1. 在隔离环境里把本章三段示例跑一遍,记下解释器实现、版本和依赖来源。
  2. 给核心行为补上正常、空输入、失败和重复执行四类测试,先看到红灯再改实现。
  3. 清掉缓存和临时文件后重跑,证明结果不靠工作区里的残留。
  4. 为历史命令写出现在该走的替代路径,并说清保留了哪些架构不变量、放弃了哪些旧的安全默认。

迁移决策题

假设团队在维护一个跑了多年的 Python 服务,它仍靠本章对应的老工具,但业务不能停机。别急着直接重写。第一步列出 TDD 原则承担的真实输入和输出,再用验收测试与单元测试识别构建或运行时依赖;第二步把标准测试工具放进隔离实验,证明当前行为与失败类型;第三步用 Fake 与 Mock 设计兼容层,让旧入口和新入口在同一组契约测试下跑;最后用文档驱动开发保存制品摘要、性能或行为差异与回滚条件。只有新路径在正常、边界和故障输入上都达到既定条件,才逐步切流量或调用者。这样迁的是可验证契约,而不是把一个旧命令盲目换成一个新命令。

常见误区

误区 1

现象 → 测试在开发机上通过,CI 上失败 原因 → 测试依赖开发机缓存或隐式环境状态 修法 → 每次测试在干净环境运行,用 fixture 隔离文件、网络与临时目录

误区 2

现象 → 改一处实现,大量测试同时失败 原因 → 测试过度 mock 调用细节,实现重构即脆性断裂 修法 → 优先断言可见结果,只有协议契约本身重要时才锁定交互

误区 3

现象 → 覆盖率 100% 仍出生产 bug 原因 → 只测了快乐路径,未覆盖边界、失败与重复执行 修法 → 边界实验至少含正常、空输入、上限、依赖失败和重复执行

误区 4

现象 → doctest 失败但没人理 原因 → doctest 未纳入 CI 流水线,文档示例漂移成噪音 修法 → python -m doctest 加入构建步骤,失败即阻断合并

本章回顾

本章逐项覆盖 TDD 原则、验收测试与单元测试、标准测试工具、Fake 与 Mock、文档驱动开发。做法是先用可读接口把责任说清楚,再用边界测试把失败语义证死,最后靠可重建的环境和制品证据把结论闭环。

小结

  • TDD 三阶段:先写失败测试,最小实现,再重构
  • 验收测用户边界,单元测局部规则,集成测适配层
  • fake 记录状态断言结果,mock 验证交互,优先断言结果
  • doctest 把文档示例变执行证据,适合稳定小接口
  • 复杂搭建代码移入测试模块,避免文档被淹没

练习与验收

练习

问题 1: TDD 的 GREEN 阶段为什么只写“刚好让测试通过”的代码?

问题 2: 什么情况下用 mock 而非 fake?

问题 3: 为一个“分页查询”函数编写 TDD 测试集:先写失败测试再写实现,要求覆盖空结果、单页满、跨页边界三种情况,并用 fake 存储替代真实数据库。(独立实现)

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

TDD 三阶段

写代码的三步固定动作:先写一个注定失败的测试(红灯),再写刚好让它通过的最小代码(绿灯),最后在不破坏测试的前提下整理代码(重构)。反复走这三步就叫测试驱动开发。

验收测试

站在用户那边,从最外面的入口把整条路走通,证明“用户要的事真做成了”。它跑得慢,但回答的是“能不能用”。

单元测试

只盯一小段代码,喂几个输入看输出对不对,跑得飞快,专门抓局部规则的小错。

fake

一个“能干活但偷工减料”的替身——比如不真连数据库,而是把数据塞进一个列表里。测试时用它,能断言最后存了什么,又不用真依赖外部。

doctest

把写在文档或函数说明里的示例代码真跑一遍,输出和文档写的一致就过,不一致就报错。文档和测试是同一份,不会各写各的。

资料与写作方式声明

本章以Tarek Ziade《Expert Python Programming》合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…