TDD项目实践(一)
TDD项目实践(一)
学习背景
学习原因:自己在开发过程中,一般都是先写总体架构模块,再写模块细节,最后写测试。但发现这样的开发方式有个问题,那就是最后自己在写测试时,总有一种惰性思维,觉得自己流程已经写好且能跑了,应该没有什么错了。写的测试也会根据自己所谓的“正确”思路进行展开,造成一些错误没有注意到。因此学习了TDD(Test-Driven Development,TDD),虽然这是一门很老的开发设计方式,但也被称之为最具效能的一种开发方式。
使用资料:徐昊 · TDD项目实战70讲_重构_测试驱动开发_TDD_单元测试_徐昊_徐八叉_伦敦学派_经典学派_Kent Beck_Clean Code_重构到模式-极客时间
总的来说,这个课程偏实战,目前只学了第一个项目,还在学第二个项目。老师真的对代码那种精益求精的态度,还有“出神入化”ide a适用技巧,跟着做一下还是能学到不少的。但有时老师的思维有点太快了,还是有点跟不上。
个人相关repo:
GitHub - xjlgod/geektime-tdd-learn: 徐昊TDD项目实战 代码实现
TDD流程
验证测试与定位测试,贯穿了整个软件构造的过程。测试构成了整个开发流程的骨架,功能开发可以看作填充在测试与测试之间的血肉。这就是测试驱动开发的核心逻辑:以测试作为切入点,可以提纲挈领地帮助我们把握整个研发的过程。
TDD的完整流程可以分为以下几步:
- 理解需求,把需求分为一系列的功能点。
- 根据功能点进行进一步上下文的划分,称之为功能上下文。
- 实现每个功能上下午包含的任务项,首先编写测试,并实现为了通过测试的代码,不断重复此过程,重构代码。
- 红:编写一个失败的小测试,甚至可以是无法编译的测试;
- 绿:让这个测试快速通过,甚至不惜犯下任何罪恶;
- 重构:消除上一步中产生的所有重复(坏味道)。
在课程中,老师提出了一套任务分解法,将任务列表作为 TDD 的核心要素。
- 大致构思软件被使用的方式,把握对外接口的方向;
- 大致构思功能的实现方式,划分所需的组件(Component)以及组件间的关系(所谓的架构)。当然,如果没思路,也可以不划分;
- 根据需求的功能描述拆分功能点,功能点要考虑正确路径(Happy Path)和边界条件(Sad Path);
- 依照组件以及组件间的关系,将功能拆分到对应组件;
- 针对拆分的结果编写测试,进入红 / 绿 / 重构循环。那么 TDD 的整体工作流程如下图所示:
TDD中的测试方式
状态验证是指在与待测系统交互后,通过比对测试上下文与待测系统的状态变化,判断待测系统是否满足需求的验证方式。状态验证是一种黑盒验证,它将测试上下文与待测系统当作一个整体。当待测系统不存在内部状态,而通过作用于依赖组件(Depended On Component)达成功能的时候,我们会从依赖组件中获取状态,以验证待测系统。
1 |
|
行为验证是指通过待测系统与依赖组件(Depended On Component)的交互,来判断待测系统是否满足需求的验证方式。行为验证背后的逻辑是,状态的改变是由交互引起的。如果所有的交互都正确,那么就可以推断最终的状态也不会错。
1 |
|
在 TDD 社区中,行为验证主要是为了降低测试成本。 状态验证是将测试上下文与待测系统当作一个整体的黑盒验证,而行为验证就是将它们看作分离组件的白盒验证。它的逻辑是通过测试功能是如何实现的,来推断结果是否正确。换句话说,行为验证本身并不能验证功能是否正确,而只能验证功能是否按照某种方式实现。如果按照某种方式实现,那么就可以推测出功能是正确的。这与 TDD 的核心逻辑就冲突了。在 TDD 的红 / 绿 / 重构中,重构要求在功能不变的前提下,改变实现方式。 而对于行为验证而言,实现方式改变就是功能改变 。因而重构就无法进行!需要重写!也就是说,行为验证会阻碍 TDD 的进行,应该使用状态验证。
值得一提的是,TDD 中的测试不是行业中所谓的“单元测试”,而是 指能提供快速反馈的低成本的研发测试,也是针对不同粒度单元的功能测试 。我们要从发现问题和定位问题的角度出发,去理解和思考每一个测试的功效。
TDD的极限
让 TDD 丧失驱动力最简单的办法,就是指明某个单元内的实现细节。比如,使用冒泡法对数组进行排序。因为从功能角度来说,冒泡法还是快排序,是没有差别的:
1 |
|
如果我们需要在测试中体现不同排序算法的差异,以驱动不同的实现,那么就需要改用行为验证。测试驱动开发的主要关注点在于功能在单元(模块)间的分配,而对于模块内怎么实现,需要你有自己的想法。
TDD中的重构
TDD 是一种架构技术,它能通过测试与重构,驱动单元的划分以及功能的归属,因而是一种更为落地的架构软件的方式。
从功能测试出发,逐步完成软件开发,通过红 / 绿 / 重构循环中的重构,而不是预先设计(Upfront Design),完成功能的前提下慢慢演进出新的架构,因而也称演进式设计(Evlutionary Design)。通过重构到模式演进式地获得架构,是一种实效主义编码架构风格(Pragmatic Coding Architect)。
TDD的不同驱动方式
如果架构愿景不清晰, 那么“最晚尽责时刻”让我们不必花费时间进行空对空的讨论,可以尽早开始实现功能,再通过重构从可工作的软件(Working Software)中提取架构。 这种方式也被称作 TDD 的经典学派(Classic School)或芝加哥学派(Chicago School)。
伦敦学派的做法是这样的:
- 按照功能需求与架构愿景划分对象的角色和职责;
- 根据角色与职责,明确对象之间的交互;
- 按照调用栈(Call Stack)的顺序,自外向内依次实现不同的对象;
- 在实现的过程中,依照交互关系,使用测试替身替换所有与被实现对象直接关联的对象;
- 直到所有对象全部都实现完成。
经典学派的做法是这样的:
- 经典学派强调功能优先,设计 / 架构后置,通过重构进行演进式设计。
- 而“伦敦学派”并不排斥预先存在的设计,更强调如何通过测试替身,将注意力集中到功能上下文中的某个对象上。然后在测试的驱动下,按部就班地完成功能开发。
如果一开始就有了很好的架构规划,可以采用伦敦学派的驱动方式。
TDD的工程优势
使用 TDD 开发软件对人的要求,与其他所有软件工程方法对人的要求是一样的:理解需求,明白架构。但是 TDD 提供了这样几点在工程管理上的优势。
- 第一,理解需求等于可以针对功能点写出测试。换句话说,写不出测试就是不理解需求。不理解需求就不要开发。在不理解需求的前提下开发功能点,只能带来负的进度。从工程管理角度上看,“判断一个人是否理解了需求”的成本极高。
- 第二,不写测试,除了不会写测试之外,就是没理解需求。没理解需求就去写测试,那就是瞎干,瞎干不如不干。如果整个团队都写不出测试,那么说明这个需求无法通过可管控的工程化方式交付。
- 第三,所有软件从业人士都认为架构是重要的,但却很少有人理解架构究竟是如何发挥作用的。架构并不是停留在纸面上的框图,而是约定了构成软件系统的组件,以及组件之间的交互方式。也就是说,架构是组件职责划分的依据以及组件的交互模式。 TDD 则可以通过功能上下文以及任务项拆分的情况,判断成员是否认同并理解了架构。 如果团队已经形成了架构共识,那么对于相同的功能点,团队中所有成员拆分出的功能上下文应该也相同。因而,从工程管理的角度,TDD 降低了“判断团队是否对架构达成了共识”的成本:看功能上下文拆分就行。
- 第四,架构愿景很难在一开始就想得尽善尽美,随着需求发展,总会出现以当前架构愿景不容易实现的需求。如果硬拗进当前架构,就会出现不当的职责划分和别扭的组件交互,这只会加速架构的腐化。而“发现当前架构愿景不容易实现的需求”成本极高。 TDD 则可以通过功能上下文以及任务项拆分的情况,判断架构是否能够实现当前需求。 如果无法拆分出合理的功能上下文和任务项,那么这个需求,就是当前架构愿景不易实现的需求。因而,从工程管理的角度,TDD 降低了“发现当前架构愿景不容易实现的需求”的成本:看功能上下文拆分就行。