---
area: "society-thinking"
category: technology
companies_orgs:
- NASA
- Chrysler
date: 2025-10-31
draft: true
guest: ''
insight: ''
layout: post.njk
media_books:
- 《让编程再次伟大#44》
- 《让编程再次伟大#26》
people:
- Kent Beck
products_models:
- UML
- RUP
- PostgreSQL
project: []
series: ''
source: https://www.youtube.com/watch?v=TzOaXepgEtQ
speaker: Peter Pang
status: evergreen
summary: 本期视频深入探讨了测试驱动开发（TDD）这一软件开发模式的起源、发展与最终式微。从NASA的水星计划到Kent Beck的极限编程（XP），TDD曾被寄予厚望，但其过度耦合代码实现细节、忽视需求动态变化的本质缺陷，导致了项目失败和开发者的普遍抵触。视频分析了TDD为何将测试从守护者变为“最讨厌的邻居”，并分享了作者基于容器技术和属性测试的实践，旨在重新审视测试在软件生命周期中的正确位置。
tags:
- canada
- development
- management
- software
title: 测试驱动开发（TDD）的兴衰：程序员为何不爱写测试？
---
### TDD的定义、历史与核心困境

**测试驱动开发**（TDD: Test Driven Development，一种以测试驱动开发的软件开发模式）简单来说，是一种在拿到开发需求后，先凭空把测试写出来的方法。由于此时还没有实际代码，测试必然会失败。接下来的工作就是编写实际代码，让测试结果的红灯一个个变绿，直到全部通过。之后，在对代码进行重构或增加新功能时，也要确保已有测试继续保持绿灯状态。

作为一名程序员，你或多或少都听说过TDD，但你们中的大部分人应该都没有真正实践过TDD。毕竟这种做法还是有点极端，与大家平常的开发模式区别较大。愿意冒着风险尝试的，一般情况下都是因为项目负责人自己就是TDD的“教徒”。这类人相对而言不多，所以与前两期的**UML**（Unified Modeling Language: 统一建模语言，一种用于软件系统可视化、规范、构造和文档化的通用建模语言）和**Agile**（敏捷开发: 一种以迭代、循序渐进的方式进行软件开发的管理方法）相比，TDD在计算机行业的影响力没有那么大。

但TDD同样是90年代个人计算机热潮中诞生的软件开发理念。TDD之于代码测试，就像UML之于系统设计，Agile之于项目管理，都具有历史代表意义。所以TDD绝对值得一期“墓志铭”。而且从它的失败中，我们还能尝试解答一个困扰大家已久的问题：为什么我们都不想写测试，都不想更新它、修复它？即使我们在日常工作中，最讨厌的就是别人不写、不更新、不修复测试。测试本应该是我们的守护者，却活成了我们最讨厌的邻居。答案，就在TDD犯下的错误里。

### NASA水星计划与XP模式的诞生

在60年代，美苏冷战进入到比拼载人航天的环节。**NASA**（美国国家航空航天局）的**水星计划**（Mercury Program: 美国在冷战时期旨在实现载人航天的早期太空计划）只有一个硬指标，就是要比苏联快。所以他们的软件开发部门被设计成开发和测试并行的组织结构。一旦产品的功能需求文档定稿，就会被同时发送给开发组和测试组，两边同时开工，最后“顶峰相见”。

作为美国历史上最成功的工程项目，水星计划中用到的诸多项目管理方式，也自然成为后人借鉴的模板。这其中就有一位叫做**Kent Beck**的青年。没错，他就是上期视频的男主角，**eXtreme Programming**（XP: 极限编程，一种强调极限实践的软件开发方法论）模式的创始人。他很喜欢借鉴古人的做法，然后再把它们推到“极限”的程度，这也是“extreme”这个词的来源。

当他在**克莱斯勒**的项目组里打造XP模式时，在测试这个工作上，他也借鉴了NASA的做法，只不过他更极端，直接把测试的位置提到了最前沿。在他的模式下，需求文档确认后，首先要做的就是用**单元测试**（Unit Test: 针对程序最小可测试单元进行验证的测试方法）把所有可能出现的状况都测一遍。这相当于用测试代码将需求文档翻译了一轮。那么接下来入场的开发，就不再是由需求文档来引导，他们唯一的任务就是写下能让这些测试通过的代码。这就是测试驱动的开发，TDD的来源。即使在XP全家桶里，TDD也算是比较极限、比较极端的那个。

因为大多数XP理念只是一种指导型方案，只包括对大方向的描述，具体怎么做还得看程序员自己的理解和开发团队的相互配合。但TDD是一个非常具体的方案，它基本上是在手把手教你怎么写代码。它会深入影响每一个人的编程风格，你每次敲击键盘都要遵循它的规则。这也是为什么喜欢它的人很喜欢，讨厌它的人很讨厌。那些喜欢TDD的，通常都会非常积极地向周围的人推荐TDD的做法。具体原因可以参照人类宗教史，某种程度上这也造成了TDD无处不在的假象，因为它的存在感确实是比它的实际存在要大很多。

### TDD的局限性与失败

就像在上上期里出自于**RUP**（Rational Unified Process: 统一软件开发过程，一种迭代的、以用例为驱动的、以架构为中心的软件开发过程）管理模式的UML，TDD也因为它广泛的通用性脱离了XP，成为一个独立传播的开发模式。只不过，它的传播显然没有UML那么成功。作为一个诞生在90年代的开发模式，TDD的第一个国际级会议是在2021年才召开的。14位演讲者通过线上会议的形式，从各个角度分享了自己的TDD成功故事，以及为什么大家都应该用TDD。截止至本视频发布，据我所知，这也是TDD的最后一个会议。

在单元测试层面定下代码要求，在有经验的人听起来就像是一个坏主意。因为这相当于让具体代码的实现细节和测试过度耦合了，会锁死代码之后的调整空间。而作为首个应用TDD的软件项目，Kent Beck主导开发的克莱斯勒员工薪酬管理系统，在第一个版本交付后就因为员工过劳，无法继续。两年后就被关停了。作为XP发源地的克莱斯勒也宣布，集团内部禁止一切XP模式。

不知道是不是受到这次失败的教训影响，TDD的执行也开始从单元测试层面往上走。他们不再追求最小单位的测试，而是转向更大范围的，在需求层面的测试。这也衍生出了**验收测试驱动开发**（ATDD: Acceptance Test Driven Development，一种将单元测试替换为面向客户验收测试的开发模式），将单元测试替换成面向客户的验收测试。但不管测试的范围怎么调整，TDD有一个本质是不变的，那就是尝试在实际开发开始之前，在测试代码中想象和框定最终成果。

### 软件需求：一个时刻变化的变量

这种透支未来的做法，听起来是不是有点既视感？因为这也是前两期“墓志铭”主角UML和Agile在做的事情。不管是代码测试、系统设计还是项目管理，正常的顺序都应该是先有需求，然后有实现，最后有结论。但TDD、UML和Agile想的都是把后面两步反过来，先下结论，再去实现。这样是不是就能运筹帷幄，事半功倍了呢？

这种抄捷径的想法，通通忘记考虑了最重要的一个影响因素：那就是在软件项目中，“需求”不是一个常量，而是一个时刻在变化、在游走的变量。不管这个需求是来自业务方的功能需求、来自领导层的业绩压力，还是来自生产环境的安全隐患，它往往都是一个移动靶。当你的理论是基于一个假设的常量，然后在这个常量的基础上画坐标，在这个坐标上建公式，最后自证这个公式在理论上成立，那么在你发现这个常量不存在，固定的坐标也不存在的时候，你的公式就会出现越来越大的偏差，直至无法使用。牛顿他老人家就吃过这个亏。

### 重新审视测试的价值与定位

测试本应该是我们的好朋友，前提是摆正它的位置。在TDD眼里，测试是一种代码化的需求文档。比起普通的需求文档，它过于具体，没有解读空间，让写代码的程序员觉得自己就是个工具人。而且不管是下到单元测试，还是上到验收测试，TDD提前定好了各种函数和功能接口的做法，会让后续开发中任何的调整都十分吃力。而且很多项目的管理团队会把测试定义为一种KPI，将其包括在交付的指标中。所以，很多测试工作只是在纯粹地刷数据，比如经典的100%测试覆盖。它们的价值定位被扭曲，也不怪程序员嫌弃了。

在我看来，测试本质上就是一种模拟。测试的价值就是可以伪装成真正的客户，模拟使用代码和功能，模拟检验返回的结果。这个模拟的场景越真实，越接近现实的生产环境，效果自然越好。我们见过很多测试是通过了，但是在生产环境还是出bug的情况，这是因为测试的场景和实际场景不一样。所以我不会在项目的设计阶段，甚至是开发初期考虑测试问题。而是在软件初见雏形、设计基本定型、大致流程跑通、可以看到结果之后。因为在这个时候，我有了一个基本能用的产品，可以想象得到真实的使用场景是怎么样的，这个产品在哪里最薄弱，哪里交互最复杂，哪里出错最危险，都已经有一个大概的范围。就像著名程序员**Atomic Energy**说过的：“the moment you know what to fear is the moment you know what to test”。

这种理念听起来很凭感觉、很不客观，但如果你有看完前两期“墓志铭”，你应该能够感受得到，软件工程就是无法完全客观的东西。所以我觉得在大方向上制定规则，然后在具体执行上让有经验的人自主决定，才是最优解。

### 作者的测试实践分享

我简单分享一下我的做法，不是为了“安利”，只是拿来跟TDD做一下对比，孰好孰坏，你们自己决定。我这里有一个系统，它包括了**PostgreSQL**（一种开源的关系型数据库管理系统）、**Redis**（一个开源的、内存中的数据结构存储系统，可用作数据库、缓存和消息代理）、一个前端API、一个支付API、一个异步工作机（Async Worker）。这里只有前端API是面向客户的。

回到二三十年前，因为技术限制，我们没有能力在本地模拟全套的系统交互逻辑。所以要么只做一些简单的单元测试，要么就用各种mocking框架来做假惺惺的所谓模拟。但现在**容器技术**（Container Technology: 一种轻量级的虚拟化技术，允许应用程序及其依赖在隔离的环境中运行）已经很成熟了，所以在本地一比一还原生产环境只需要一个简单的**Docker Compose**（Docker Compose: 用于定义和运行多容器Docker应用程序的工具）配置文件。不熟悉容器技术的可以去看《让编程再次伟大#26》的介绍。

模拟环境搭建好后，我用的是一种叫做**基于属性的测试**（Property-based testing: 一种测试方法，通过生成大量随机输入来验证代码是否符合预设的属性或规则）的测试手法。具体做法就是模拟真实客户可能做的操作，随机生成各种输入参数，随机调用各个前端API，然后针对系统返回的结果当场判断它是否符合所测功能应有的特性。比如说，某个随机生成的输入是不合要求的，那么按照这个功能的特性，就应该返回400。又比如某个API的调用顺序不对，就应该返回403之类的。这样我们就不需要手搓test case，而是随机模拟，从而把所有乱七八糟的可能性都测一遍。

因为整个测试环境都在本地，所以响应速度也很快。普通笔记本也能在1秒内模拟100个test case。在稍微好一点的台式机上，我们还能够搞个100倍并发，顺便也把压力测试给做了，一石二鸟。这种测试我们写起来很积极，维护起来也很认真。因为我们知道，只要这些测试能通过，我们就很有信心，这个系统上线之后不会被客户玩坏，我们晚上就能安睡。而这，就是我们的测试之道。