理解TDD的好处和使用
了解了 TDD 的基本原理和最佳实践后,接下来我们将更深入地探讨在团队中采用 TDD 作为实践的好处。由于敏捷工作实践已经成为行业标准,我们将继续讨论敏捷团队中如何使用 TDD。
将 TDD 纳入开发过程,立即使开发人员能够更轻松地编写和维护测试,同时也能更容易地检测和修复 bug
。
使用TDD的优缺点
图1.7展示了使用 TDD 的一些优缺点:

我们可以扩展这些优缺点的要点:
-
TDD让开发和测试过程同时进行,确保从一开始就测试所有代码。虽然 TDD 确实需要在前期编写更多的代码,但编写的代码会立即覆盖在测试中,并且开发人员在实现代码时可以及时修复
bug
。测试不应是事后的思考,也不应因为实现的延迟而被仓促或删减。 -
TDD让开发人员在每个迭代的开始就能详细分析项目需求。虽然它确实要求产品经理在迭代规划阶段确定需要构建的细节,但它也使得开发人员能够对每个迭代中可以实现的功能提供早期反馈。
-
使用TDD编写的经过良好测试的代码可以放心地交付和更改。一旦代码库建立了测试套件,开发人员就可以自信地更改代码,因为现有功能不会被破坏,因为测试失败会在更改发布之前标明任何问题。
-
最后,最重要的优点是,它使开发人员对代码质量拥有更大的责任感,让他们对实现和测试都负有责任。编写代码时编写测试代码可以让开发人员在短时间内获得反馈,及时发现代码的潜在问题,而不是在发布完整功能后很久才知道自己哪里做错了。
在我看来,使用 TDD 的最重要优势是增强了开发人员对代码的责任感。即时反馈循环使他们能够发挥最佳表现,同时也让他们放心,自己没有破坏现有的代码。
现在我们已经了解了 TDD 及其优点,让我们通过一个简单的计算器示例,探讨 TDD 的基本应用。
用例 – 简单的终端计算器
这个用例将帮助你深入理解我们在测试更复杂示例时将要采取的通用过程。
我们将看一个简单的终端计算器作为用例。该计算器将在终端中运行,并使用标准输入读取其参数。计算器将只处理两个运算符和图1.8中所示的简单数学运算:

这个功能很简单,但计算器还应该能够处理边界情况和其他输入错误。
需求
敏捷团队通常从用户的角度编写需求。项目的需求首先被写下来,以捕捉客户的需求,并指导整个简单计算器项目的测试用例和实现。在敏捷团队中,需求经历多次迭代,工程领导层会提前介入,以确保所需的功能能够交付。
用户应该能够执行以下操作:
-
使用终端输入正数、负数和零值。这些值应该正确地转换为数字。
-
访问加法、减法、乘法和除法的数学运算。对于所有输入,这些运算应该返回正确的结果。
-
查看四舍五入到小数点后两位的分数结果。
-
查看用户友好的错误消息,引导他们如何修复输入。
从用户角度看待的敏捷需求
需求用于捕捉最终用户的需求和观点。需求定义了前提条件、用户操作和验收标准,明确了我们应该构建什么以及如何验证实现。 记住,我们只在每个迭代周期中指定需求。事先指定整个产品的需求,或假设需求不能改变是反模式。敏捷软件开发是一个迭代过程。 |
架构
我们的简单终端计算器足够小,可以在一个迭代周期内实现。我们将四个需求转化为一个简单的系统架构。计算器将被用户下载并本地运行,因此我们不需要考虑任何网络或云部署的方面。

计算器模块的每个组件都有自己明确的责任和功能:
-
输入解析器负责与终端输入进行集成,正确读取用户输入并将其传递给计算器模块。
-
输入验证器负责验证输入解析器传递的输入,例如检查输入是否包含有效的数字和运算符。
-
一旦输入被解析和验证,计算器引擎接受数字并尝试计算操作的结果。
-
然后,计算器引擎依赖于结果格式化器来正确格式化结果并将其打印到终端输出。如果发生错误,则依赖错误格式化器生成并打印用户友好的错误消息。
应用TDD
如上所述,我们将使用红、绿、重构的过程,应用 TDD 以迭代方式交付所需的用户功能。测试首先根据简单终端计算器的需求和设计编写。
以下是如何将 TDD 过程应用于计算器引擎中的 Divide(x, y)
函数的概述,如图 1.10 所示:

这是一个小快照,展示了使用 TDD 时涉及的步骤:
-
我们首先编写一个简单的
TestDivide()
,安排两个非零输入,并编写断言来对它们进行除法运算。这是我们可以实现的最简单的情况。然后,我们运行测试套件,确保新编写的TestDivide()
失败。 -
现在,测试已经确立了预期的行为,我们可以开始实现
Divide(x, y)
函数。我们编写足够的代码来处理两个非零输入的简单情况。然后,我们运行测试套件,验证我们编写的代码是否满足TestDivide()
的断言。现在所有的测试都应该通过。 -
我们可以花些时间来重构已经编写的代码。新编写的代码可以根据干净代码的实践和我们讨论的 TDD 最佳实践进行清理。测试套件再次运行,以验证重构步骤没有破坏任何新旧测试。
-
Divide(x, y)
函数的最简单功能现在已经实现并验证。我们可以开始查看更高级的功能或边界情况。一个这样的边界情况可能是优雅地处理零除数。我们现在添加一个新的测试TestDivide_Zero()
,设置并断言零除数的情况。像往常一样,我们运行测试套件,确保新的TestDivide_Zero()
测试失败。 -
我们修改
Divide(x, y)
的实现,以优雅且正确地处理零除数,正如计算器需求所规定的(如果必要,与产品负责人甚至用户沟通)。我们再次运行测试,确保现在所有的测试都通过。 -
最后,我们开始新一轮的重构,确保代码和测试编写良好。所有的测试再运行一次,确保重构没有导致任何错误。
TDD 成为习惯
开发过程会根据需要在编写测试代码和编写实现代码之间多次切换。虽然一开始可能看起来有些繁琐,但对于 TDD(测试驱动开发)实践者来说,这种切换很快就会变得自然而然。 始终记住,首先要从一个失败的测试开始,然后编写尽可能少的代码使测试通过。只有在重构阶段,当你确认所有功能都正常工作后,才去优化你的代码。 |
现在我们已经熟悉了 TDD 的过程,并学习了如何相应地编写和构建我们的测试。然而,也有必要考虑其他的开发过程。