← 返回题目列表

什么是 TDD?在 Python 项目中如何实践?

高频 中等 第 4 / 27 题 更新于 2026/07/27
TDD测试驱动开发设计

简化版

TDD 是测试驱动开发,流程是先写失败测试,再写最少代码让测试通过,最后重构。它的价值不只是补测试,而是通过测试先定义行为,倒逼代码接口更清晰、边界更明确。

详细版

TDD 常见节奏是 Red-Green-Refactor:

  1. Red:先写一个失败测试,描述期望行为;
  2. Green:写最少实现让测试通过;
  3. Refactor:在测试保护下重构代码。

Python 中可以用 pytest 实践:

def test_discount_for_vip():
    assert calculate_price(100, vip=True) == 80

然后再实现:

def calculate_price(price, vip):
    return price * 0.8 if vip else price

TDD 适合业务规则清晰、边界条件多、需要稳定演进的模块,不适合需求极不稳定或探索性很强的代码一开始就强行套用。

完整版教学

一、TDD 和“写完代码再补测试”有什么不同

很多团队说自己有测试,其实是代码写完后补几条用例验证一下。TDD 则反过来:先写测试,再写实现。

这个顺序变化很重要。先写测试时,你必须先想清楚代码应该暴露什么接口、输入是什么、输出是什么、异常怎么处理。测试变成了行为规格说明。

例如要写一个优惠计算函数,你先写:

def test_vip_gets_20_percent_discount():
    assert calculate_price(100, vip=True) == 80

此时 calculate_price 还不存在,测试失败。这个失败不是坏事,它说明你已经定义了第一个行为。

二、Red-Green-Refactor 循环

TDD 的经典节奏是 Red、Green、Refactor。

Red 阶段:写一个失败测试。失败原因应该明确,最好是因为功能还没实现,而不是测试写错。

Green 阶段:写最少代码让测试通过。不要一上来设计复杂架构,只实现当前测试需要的行为。

Refactor 阶段:在测试通过的保护下清理代码,消除重复、改善命名、抽取函数、优化结构。

这个循环让代码逐步生长,而不是一次性脑补完整设计。

三、TDD 如何改善设计

如果一个函数很难测试,往往说明它设计上有问题。例如它直接读环境变量、直接访问数据库、直接调用第三方接口、内部逻辑又长又复杂。为了写测试,你会自然地把依赖抽出来,把纯逻辑和副作用分离。

例如原始代码:

def create_order(user_id, sku):
    user = db.get_user(user_id)
    price = remote_price_service.get_price(sku)
    db.save_order(user, sku, price)

它不好测,因为数据库和远程服务都耦合在里面。为了测试业务规则,可以把价格计算、库存校验、订单创建拆成更清晰的 service,并通过依赖注入传入外部服务。

所以 TDD 的价值不只是多了测试,而是推动代码变得低耦合、高内聚、边界清楚。

四、Python 中如何开始实践 TDD

可以从纯函数或业务 service 开始,不要一开始就挑战完整 Web 流程。比如金额计算、权限规则、状态转换、解析逻辑,这些都很适合 TDD。

流程可以是:

  1. 写一个最小失败用例;
  2. 运行 pytest,确认失败;
  3. 写最简单实现;
  4. 再运行 pytest,确认通过;
  5. 补边界用例;
  6. 重构代码。

示例:

import pytest

def test_rate_must_between_0_and_1():
    with pytest.raises(ValueError):
        calculate_discount(100, 1.5)

这个测试推动你明确异常规则,而不是让非法输入悄悄产生错误结果。

五、TDD 不适合被神化

TDD 很有价值,但不是所有代码都必须严格先写测试。探索性原型、一次性脚本、需求还在剧烈变化的实验代码,强行 TDD 可能拖慢反馈。

更现实的做法是:核心业务规则、复杂分支、长期维护模块优先 TDD;快速探索代码可以先验证方向,稳定后再补测试和重构。

成熟团队不会把 TDD 当宗教,而是把它作为设计和质量工具。

六、面试如何回答更稳

面试中可以这样表达:TDD 的核心不是追求测试数量,而是用测试先描述行为。它通过 Red-Green-Refactor 小步迭代,让代码在可验证状态下演进。

还可以补充工程实践:TDD 要配合 CI、覆盖率、代码评审和良好的测试命名。测试名称应该描述业务行为,例如 test_admin_can_delete_user,而不是 test_001

七、常见误区与追问

阶段目标不该做的事
Red写一个明确失败的行为测试一次写一堆失败测试
Green用最少实现让测试通过立即做复杂架构设计
Refactor在测试保护下改善结构改行为却不补测试
第 1 步:写 test_vip_gets_discount,失败
第 2 步:实现最小折扣逻辑,通过
第 3 步:补非法折扣率测试,失败
第 4 步:加参数校验,通过

记忆钩子:TDD 不是“先写测试文件”,而是用失败测试驱动一个可验证的小行为生长出来。

第一个误区是把 TDD 理解成写测试工具。TDD 是开发节奏和设计方法。

第二个误区是测试写得太贴实现细节。好的测试关注行为,不应该因为内部重构就大量失败。

第三个误区是一次写很多失败测试。TDD 强调小步快跑,一个行为一个行为推进。

第四个误区是 Green 阶段写过度设计。先让测试通过,再在测试保护下重构。

  • 误区:TDD 等于提前写完所有测试。 TDD 强调小步循环,一个行为一个失败测试,随后实现和重构。
  • 误区:测试越贴内部实现越精确。 好测试关注外部行为和业务契约,内部重构不应该导致大量无意义失败。
  • 误区:Green 阶段就要设计最终架构。 Green 只写足够让当前测试通过的实现,结构优化放到 Refactor 阶段。
  • 追问:TDD 为什么能改善设计? 难测试的代码往往依赖耦合重、副作用多;为了先写测试,开发者会自然抽出接口、隔离副作用、明确边界。
  • 追问:哪些 Python 场景适合 TDD? 金额计算、权限规则、状态机、解析器、长期维护的 service 模块都适合;探索性原型不必强行套用。
  • 追问:TDD 和覆盖率是什么关系? TDD 往往带来较好覆盖,但目标是用测试定义行为和保护重构,不是机械追求覆盖率数字。

八、加强记忆

记 TDD:先用失败测试定义行为,再用最少实现让它通过,最后在测试保护下重构。它最大的收益是倒逼接口清晰、依赖可替换、边界明确,而不是简单把测试提前写。