主题
11-作为工程化方法的TDD:更低的成本与更高的效能
你好,我是徐昊。在了解了测试驱动开发中的“测试”,和测试驱动开发中的“驱动”之后。让我们重新复盘一下作为工程化方法的TDD。
TDD的流程
在第一讲中,我曾给出了一张任务分解法的流程图。用以在TDD演示过程中,帮助我们把握整体流程的走向。由于是在课程刚开始给出的,很多内容我们尚未澄清,无法给出更细致的描绘,只能看做对于TDD流程大略的描述。
那么在学习了什么是TDD中的测试,和什么是TDD中的驱动之后,我们将会怎么描述TDD的流程呢?

如上图所示,使用TDD的核心流程为:
- 首先将需求分解为功能点,也就是将需求转化为一系列可验证的里程碑点;
- 如果已经存在架构或架构愿景,则依据架构中定义的组件与交互,将功能点分解为不同的功能上下文;
- 如果尚不存在架构愿景,则可以将功能点作为功能上下文;
- 将功能点按照功能上下文,分解为任务项。也就是进一步将可验证的里程碑点,分解为功能上下文中可验证的任务项;
- 将任务项转化为自动化测试,进入红/绿/重构循环,驱动功能上下文内的功能实现;
- 如果重构涉及功能上下文的重新划分,即提取/合并组件,即视作对于架构的重构与梳理。需调整后续功能点中对于功能上下文以及任务项的划分。
- 如此往复,直到所有功能完成。
通过上述过程的描述,可以发现任务列表中的任务项源自两层分解:源自对于业务理解的功能点分解,以及源自架构愿景的功能上下文分解。
功能点分解帮助我们形成可验证的里程碑点。这些里程碑点可看作由可工作的软件(Working Software)构成的进度度量。功能上下文分解帮助我们找到正确的单元,指导我们保持良好的软件架构。
如果功能点分解错误,那么就得不到功能正确的软件;如果功能上下文分解错误,那么就得不到架构良好的软件。
TDD的工程优势
所以,使用TDD开发软件对人的要求,与其他所有软件工程方法对人的要求是一样的:理解需求,明白架构。但是TDD提供了这样几点在工程管理上的优势。
第一,理解需求等于可以针对功能点写出测试。换句话说,写不出测试就是不理解需求。不理解需求就不要开发。在不理解需求的前提下开发功能点,只能带来负的进度。从工程管理角度上看,“判断一个人是否理解了需求”的成本极高。我想大家都有在各种拉通会议中,反复宣讲确认,但仍如鸡同鸭讲,不断消磨精力与斗志的体验。
TDD的“先写测试,后写生产的代码”的实践,实际上也就是要求“先理解需求,再开发”。但是TDD对于什么叫理解了需求,有了清晰的要求:能写出这个功能点的测试,就是理解了需求,别的都白扯。因而TDD从工程管理的角度,降低了“判断一个人是否理解了需求”的成本:看测试就行。
第二,不写测试,除了不会写测试之外,就是没理解需求。没理解需求就去写测试,那就是瞎干,瞎干不如不干。如果整个团队都写不出测试,那么说明这个需求无法通过可管控的工程化方式交付。
可管控的工程化方式交付,意味着这个需求在实现层面上可以被执行,也就是高确定性的。在高确定性的环境下,要追求效率。
而无法通过可管控的工程化方式交付,意味着不确定这个需求在实现层面上是否可被执行,需要进入探索模式。在不确定的情况下,要追求低成本及时止损。
所谓探索模式,也就是原型验证、定向的深度搜索、聘请外脑之类的活动。探索模式的关键在于,要在给定的时间范围内尝试,比如一到两天。超出这个时间范围仍然无法解决的,将变成项目风险,需要额外的管理方法,比如上升。然而,从工程管理的角度,“判断哪个需求应该从工程化方式交付模式进入探索模式”的成本极高。通常已经形成进度阻塞、延期或大量返工的时候,才能发现根本原因是不能确定实现方式。
TDD可以通过是否可以在规定时间内写出测试加以判断。因而,从工程管理的角度,降低了“判断哪个需求应该从工程化方式交付模式进入探索模式”的成本:看测试就行。
第三,所有软件从业人士都认为架构是重要的,但却很少有人理解架构究竟是如何发挥作用的。架构并不是停留在纸面上的框图,而是约定了构成软件系统的组件,以及组件之间的交互方式。也就是说,架构是组件职责划分的依据以及组件的交互模式。
比如MVC(Model/View/Controller)架构,表明系统组件按职责应该分成三种:管理数据的模型(Model),负责展示的视图(View),以及处理事件交互逻辑的控制器(Controller)。它们的交互模式为:视图渲染模型中的数据,视图中触发的事件则交由控制器处理,控制器可以通过修改模型,或是更新视图。如下图所示:

想要破坏MVC的架构愿景很容易,比如把控制逻辑写到模型中,或者在视图中直接修改模型,都会破坏MVC的架构愿景。
架构是软件构造过程中产生的一类非常特殊的知识,它必须成为全体人员的共识,才能真正发挥作用。否则,架构就是一句空话,或者一堆空话。
从工程管理的角度,“判断团队是否对架构达成了共识”的成本极高。通常需要大量的代码审查才能发现对于架构理解的偏差,也需要大量的返工才能纠正这些偏差。
TDD则可以通过功能上下文以及任务项拆分的情况,判断成员是否认同并理解了架构。如果团队已经形成了架构共识,那么对于相同的功能点,团队中所有成员拆分出的功能上下文应该也相同。因而,从工程管理的角度,TDD降低了“判断团队是否对架构达成了共识”的成本:看功能上下文拆分就行。
第四,架构愿景很难在一开始就想得尽善尽美,随着需求发展,总会出现以当前架构愿景不容易实现的需求。如果硬拗进当前架构,就会出现不当的职责划分和别扭的组件交互,这只会加速架构的腐化。而“发现当前架构愿景不容易实现的需求”成本极高。
TDD则可以通过功能上下文以及任务项拆分的情况,判断架构是否能够实现当前需求。如果无法拆分出合理的功能上下文和任务项,那么这个需求,就是当前架构愿景不易实现的需求。因而,从工程管理的角度,TDD降低了“发现当前架构愿景不容易实现的需求”的成本:看功能上下文拆分就行。
顺便说一句,就算发现了“当前架构愿景不容易实现的需求”,也可以直接进入经典模式,通过重构抽取架构要素,演进式调整当前架构,不会影响项目进度。如下图所示:

不难发现,TDD要求先写测试,实际是以可工作的软件的形态,验证对于需求的理解;而任务分解,则是在确定单元粒度的过程中,验证对于架构的共识。只要我们承认,“理解需求,明白架构”是一切工程化软件开发的前提,那么TDD就是目前已知的效能最高的方法。
小结
到此为止,我们展示了一个完整的例子:如何通过TDD实现命令行参数解析。以此为引子,我们讲解了TDD中的测试,分别介绍了状态验证与行为验证两种验证模式。并讨论了,为什么TDD中的测试不是单元测试。
在明白了TDD中的测试是什么之后,我们讨论了这样的测试是怎样驱动软件开发的:比起单元内的功能实现,测试更容易驱动架构设计,因而TDD更应该被看做是一种架构技术。然后,我们分别介绍了通过重构演进式驱动架构的经典TDD模式,以及在一定架构愿景之下,逐步实现不同功能单元的伦敦学派TDD模式。
最后,我们解释了作为工程方法,TDD与其他工程方法并没有什么本质区别,都需要在“理解需求,明白架构”的前提下,进行软件开发。只不过,TDD在工程管理上具有更低的成本和更高的效能。
虽然TDD看起来非常地违反直觉,然而在抽丝剥茧之后,你会发现,它反而是非常传统的开发模式。围绕测试与任务列表,它严谨地将所有必须的开发活动,转化为可管理的行为。杜绝了嘴上说一套实际做一套的可能。
也正是因为TDD从骨子里是非常传统的开发模式,非常强调对于需求的理解(可测试性),以及对于架构愿景的维护(演进式)。那么想要用好TDD,仅仅关注测试是不够的,还需要在需求分解与功能上下文分解上花力气下功夫。
接下来,我们将使用一系列更为实际的例子,展示如何通过TDD来完成它们。
思考题
在团队中从哪里开始推行TDD最容易入手?
欢迎把你的思考和想法分享在留言区,也欢迎你加入我们的读者交流群。我们下节课再见!