测试驱动开发(TDD)实战指南:从红到绿的代码迭代技巧

先写测试,再写代码——红阶段的正确打开方式

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

测试驱动开发(TDD)实战指南:从红到绿的代码迭代技巧

这时候不需要写一行业务代码,直接写测试用例(以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

(0)

相关推荐