Contents

面向测试驱动开发

我是怎么理解测试驱动开发(TDD)的

最近在学习软件开发的时候,我接触到了一个很经典的概念,叫做测试驱动开发,也就是大家常说的 TDD(Test-Driven Development)

刚看到这个词的时候,我其实有点懵。

因为按我的直觉,写代码的顺序应该是这样的:

先把功能写出来,再去测试它对不对。

但 TDD 的思路正好反过来,它提倡的是:

先写测试,再写功能代码。

我当时的第一反应就是: “代码都还没写,测试怎么写?”

后来慢慢看资料、看例子、自己试着理解之后,我发现,TDD 其实不是故意把事情搞复杂,而是在提醒我们一件很重要的事:

写代码之前,先想清楚它应该做什么。

在这里聊聊我对 TDD 的理解。


TDD 到底是什么?

先说我现在最朴素的理解。

所谓测试驱动开发,就是一种开发方式:

先写测试,定义你期待的结果;再写代码让测试通过;最后再整理和优化代码。

它的重点不是“多写测试”,而是:

用测试来驱动代码的设计。

也就是说,测试在这里不是最后补上的检查步骤,而是整个开发流程的起点。

这一点,我觉得是 TDD 最关键、也最容易让人一开始不习惯的地方。


为什么“先写测试”这件事这么重要?

我后来发现,TDD 真正厉害的地方,不是它把测试提前了,而是它逼着你先思考这些问题:

  • 这个功能到底要解决什么问题?
  • 输入是什么?
  • 输出应该是什么?
  • 哪些情况算正常?
  • 哪些情况算边界?
  • 这个函数或者接口,应该怎么被别人调用?

平时我们刚学写代码的时候,很容易一上来就开写。 但很多 bug,或者后面越写越乱,往往不是因为语法不会,而是因为一开始没把需求想清楚。

TDD 其实就是一种“先想明白,再动手”的节奏。


TDD 最经典的流程:红、绿、重构

只要提到 TDD,几乎一定会讲到这三个词:

红(Red)→ 绿(Green)→ 重构(Refactor)

我觉得这三个词非常形象。

1. 红:先写一个会失败的测试

先写一个测试,描述你希望代码具备的行为。

因为这个功能还没实现,所以测试肯定会失败。 失败通常会显示成红色,所以这一步叫“红”。

比如说,你想写一个加法函数,你可以先写:

1
2
def test_add():
    assert add(2, 3) == 5

这时候 add 可能还不存在,测试当然会失败。 但这不是坏事,这反而说明你正在按 TDD 的方式走。


2. 绿:写最少的代码让测试通过

接下来再去写代码,但不是一下子写很多,而是只写刚好能让当前测试通过的那一点点。

比如:

1
2
def add(a, b):
    return a + b

再运行测试,如果通过了,界面通常会变成绿色,所以这一步叫“绿”。

这一点对我来说也挺有启发的,因为它在提醒我:

先别急着做大,先把眼前这一步做对。


3. 重构:在测试保护下优化代码

当测试已经通过以后,就可以开始整理代码,让它更清晰一些。

比如:

  • 改更好的变量名
  • 消除重复代码
  • 把一个太长的函数拆开
  • 优化结构

这一阶段不会改变功能本身,只是让代码变得更好维护。

我觉得这里特别有安全感的一点是: 因为测试已经在那儿了,所以你修改代码时,不会完全靠感觉。只要测试还通过,就说明核心行为没有被改坏。


用一个简单例子来理解 TDD

拿一个特别简单的例子来说。 假设我们要写一个函数,判断一个数字是不是偶数。

函数名叫 is_even(n)

按照 TDD 的思路,第一步不是立刻去写函数,而是先写测试:

1
2
def test_is_even():
    assert is_even(4) == True

这时运行肯定失败,因为 is_even 还没写。

然后再写实现:

1
2
def is_even(n):
    return n % 2 == 0

这时候测试通过了。

接着可以继续补更多测试:

1
2
3
4
5
def test_is_even():
    assert is_even(4) == True
    assert is_even(3) == False
    assert is_even(0) == True
    assert is_even(-2) == True

这样一步一步加下去,代码和测试会一起成长。

我觉得 TDD 最像的一点就是: 不是先憋一个“大成品”出来,而是每次走一小步,每一步都确认自己没走错。


它和“写完再测试”到底差在哪?

说实话,在刚接触的时候,我也会觉得:

“反正最后都要测试,那先写还是后写,真有那么大区别吗?”

后来我觉得,区别其实很大,而且不只是顺序问题,而是思维方式的问题。

如果是先写代码再测试,很多时候会变成这样:

  • 一边写一边猜需求
  • 写完才发现边界情况没考虑
  • 代码结构已经不太好测
  • 测试变成对现有代码的事后补救

而如果是先写测试再写代码,你会先从“我要得到什么结果”出发。 这种方式会让你更关注行为,而不是一开始就陷进实现细节里。

我现在会觉得,TDD 某种程度上是在训练一种习惯:

先定义正确,再去实现正确。


我觉得 TDD 的几个明显好处

学到这里,我能比较明显感受到 TDD 的几个优点。

第一,它会逼着我把需求想清楚

很多时候,代码写不顺,不一定是因为不会写,而是因为自己都没想清楚到底要什么。

测试其实就是一种非常具体的“需求表达方式”。 当你把测试写出来的时候,模糊的想法会变得清楚很多。


第二,它能给代码提供一种安全感

这一点我很喜欢。

因为每次改完代码,我都可以跑一下测试。 如果测试通过,我就会知道:至少我已经覆盖到的这些功能,还是正常的。

这种感觉比手动点点点、试试看要靠谱得多。


第三,它会让人更自然地小步前进

对初学者来说,一个很常见的问题就是: 总想一次写很多,最后一出错就不知道到底是哪一步出了问题。

TDD 的节奏天然就是“小步走”:

  • 写一个小测试
  • 让它通过
  • 再写下一个

这个节奏其实挺适合新手的,因为它能减少“写着写着就失控”的情况。


第四,它会反过来影响代码设计

这一点我一开始没有意识到,后来才慢慢明白。

因为测试要先写,所以你必须先考虑: 这个函数应该怎么调用,参数怎么传,返回值应该长什么样。

换句话说,TDD 会逼着你从“使用者视角”去思考代码。

而这种思考方式,通常会让函数和接口变得更自然,也更干净。


当然,TDD 也不是万能的

虽然我现在觉得 TDD 很值得学,但它也绝对不是一种“银弹”。

刚开始真的会觉得麻烦

这个感受很真实。

尤其是刚学编程的人,本来写功能就已经够费劲了,再加上写测试,确实会觉得流程变长了。

所以我觉得,初学阶段不用强迫自己在所有地方都严格套 TDD。 先在小练习里体会它的节奏,比一上来就全盘照做更现实。


不是所有场景都特别适合

比如有些场景本身就很偏探索性:

  • 需求还没稳定
  • 页面 UI 变化很快
  • 只是写个一次性脚本
  • 很多逻辑都依赖外部系统

这种时候,TDD 可能就没那么“顺手”。

所以我现在更愿意把它理解成一种很强的开发方法,而不是必须无条件套用的教条。


测试写不好,也会变成负担

这一点我觉得也挺重要。

如果测试写得太依赖实现细节,那么代码只要一重构,测试可能就全挂。 这种测试不但不能帮忙,反而会拖累开发。

所以一个很重要的点是:

测试应该尽量关注行为,而不是过度关注内部实现。


如果你和我一样是初学者,可以怎么开始?

如果你现在也是刚入门,我觉得不用一开始就拿复杂项目练 TDD。 最好的方式,就是从很小、很纯粹的函数开始。

比如这些都很适合:

  • 判断一个字符串是不是回文
  • 判断一个数是不是质数
  • 计算折扣价格
  • 校验用户名是否合法
  • 把分数转换成等级
  • 统计一句话里的单词数量

练习的时候就记住这个节奏:

先写一个失败测试, 再写最少代码让它通过, 然后再继续补测试、改实现。

真的不用一开始就搞得很复杂。


我现在怎么理解 TDD?

如果让我现在用一句最简单的话来总结,我会这样说:

TDD 就是先把“我希望代码怎么表现”写下来,再去实现它。

它最吸引我的地方,不只是“能保证质量”,而是它改变了写代码时的思考顺序。

以前我更容易直接冲进实现里。 现在我会更意识到,很多时候真正重要的问题其实是:

“这段代码到底应该做什么?”

而 TDD,就是一种不断把你拉回这个问题的方法。


最后总结一下

测试驱动开发并不只是“先写测试”这么简单。

它更像是一种开发节奏,一种思考方式:

  • 先明确预期
  • 再实现功能
  • 每次只前进一小步
  • 用测试保护自己继续重构

它的经典流程就是:

红 → 绿 → 重构

对于刚入门的人来说,我觉得 TDD 很值得了解,也很值得练习。 哪怕你暂时还不能在真实项目里熟练使用它,只要你开始接触这种思路,就已经很有价值了。

因为它在训练的,其实不只是“怎么写测试”,而是: 怎么更清楚地思考代码。