背景
最近项目中有同事做了一次 TDD(测试驱动开发) 的知识分享,编码实践中采用了保龄球的例子,可以参考保龄球积分规则。在这个过程中,他介绍了业务规则,然后列出来一些场景开始进行测试代码和功能代码的编写。
在演示完后有人提出来说一些边缘用例没有考虑,比如输入的分超过10分或者负数怎么办等。这些用例应该是在 TDD 的哪个阶段进行考虑呢,所以引发了我对 TDD 中的 tasking 的思考。Tasking(任务分解)是 TDD 的一个关键的步骤,在开始进入编码环节之前,需要对业务需求做拆分,我们把这个过程它称之为 tasking。本文将讲述 TDD 和瀑布开发的关系,TDD 中的 tasking 目的是什么,该如何进行 tasking。
TDD 与瀑布式开发
瀑布式开发
瀑布式开发(waterfall model),起源于 Winston Royce 发表于 1970 年的著名文章 "Managing the Development of Large Software Systems"。从此很多人认为 Royce 在这篇文章中,软件开发应采用严格、顺序、单次的瀑布生命周期,包括需求分析、设计、实现和测试等阶段。
前一个阶段的输出就是下一个阶段的输入,当上一个阶段出现的问题可以在下一个阶段进行验证和返工上一个阶段的问题,比如需求阶段出现问题,可以在设计阶段进行验证和修正;设计阶段出现的问题可以在实现阶段验证和修正。但是有一个例外就是到测试阶段,才可以看到软件的“模样”,这个时候是完完整整的把软件开发做了一遍,但是此时发现软件中存在问题,那有可能要到需求阶段进行验证和返工,就会导致工期延长。
那我们怎么解决这个问题,瀑布之父 Winston Royce 真正倡导的是什么?他建议的其实是 do it twice,一个两次瀑布的迭代模型。就是在文档中通过详细设计把软件做一遍,然后在真正编码实现的过程之时,我们已经在文档中做过一遍了,就是 do it once 的过程,此时我们对软件的认知水平处于高认知的状态。我们在编写功能代码就是 do it twice,此时对需求的认知已经到了明显阶段。Roy 在2000年左右的时候也提过,我们可以把“在文档中通过详细设计把软件做一遍”转变为通过实际代码来做一遍,进行迭代开发。
If the computer program in question is being developed for the first time, arrange matters so that the version finally delivered to the customer for operational deployment is actually the second version insofar as critical design/operations areas are concerned.
通过这种 do it twice,开发人员在编写功能代码之前对业务需求的认知是处于明显的阶段,很清楚知道要做的需求和确定的技术。
TDD
TDD 的流程:先将需求转换为具体的任务列表,根据任务列表把每一个任务拆分成一个或者多个测试用例,每一个测试大概在15分钟左右实现通过。实际编码过程体现为:在开发业务功能代码之前,先编写测试代码。测试代码确定了我们要验收什么以及如何验收,然后再去编写功能代码,当测试通过时,代表功能完成。
在这个过程中如果无法进行任务分解,不能将需求转化成任务列表,那么一般有两种情况:
业务需求不明确:和业务进行需求澄清,明确要做什么
对当前的技术实现和工具不清楚:此时需要进行技术预研,知道技术实现方式
实际上经过需求澄清和技术预研后的tasking就是小型版的 do it once 的一个过程,让我们对这个需求有了更高的认知能力。写测试的过程就是 do it twice,测试中给出了明确的输入输出。这个时候我们对这个需求的认知已经到了明显阶段,很清楚知道需求的验收标准和确定的技术。
TDD 中的 tasking 和先写测试代码就是用需求细节和实例来验证功能,也是从另外视角对拆解后的需求进行描述。在敏捷开发中,轻装前行是件很快乐的事情,但这不意味着丢弃一切技术预研和模型,丢弃一切需求文档(包括用户故事等)和需求澄清。
敏捷开发也是需要在前期做适当的准备,对于复杂的业务,化繁为简,层层递进。分步去试验用户的反应,从而降低市场以及产品质量等各方面的风险。我们可以根据用户和市场的反应,来更新业务需求和软件。
TDD 不能完全解决业务需求模糊甚至错误的情况,但是 TDD 的 tasking 能让开发人员确认理解了需求并可以和业务方达成一致,能帮助开发人员与业务方或测试在验收需求时确认基于共识实现了需求,能够帮助开发人员写出测试用例来驱动功能代码的编写,让我们在当前迭代开发出和业务需求最为接近的软件。
下图上半部分是 TDD 的简单的过程:tasking、红、绿和重构。红代表测试能够运行,但是断言失败;绿代表实现业务代码,刚才失败的断言通过;重构代表有意图的对代码进行重构,提高可读性。
TDD 和瀑布开发不是处于同一个层面的事物,TDD 是在开发的阶段,需求可能只是一个用户故事;而瀑布开发是软件系统开发的生命期模式,需求可能是一个庞大的软件系统。然而从上图中可以看出,TDD 中 tasking 和瀑布中的设计可以看作 do it once,让我们对需求认知从复杂阶段到了繁杂阶段,到了最后写功能代码可以看作 do it twice,我们的认知水平处于明显阶段。通过层层问题的降解,使我们在写功能代码的时候我们已经很清楚知道需求的验收标准和要使用的技术方式,让我们的对问题的认知水平得到了提高。此时我们是可以评估出来工作量的大小,并且根据需求的验收标准来实例化对应的业务场景。
所以我们可以看出在 TDD 和瀑布开发都采用了化繁为简,二者都是通过一定的手段,让我们对需求的认知水平逐步提高,从复杂到繁杂再到明显,在一定程度上可以说 TDD 与瀑布开发有很大的相似性,甚至可以说“殊途同归”。
在上面提到了 tasking 是检验我们第一个 do it once 是否成功,如果没有 tasking 直接进入第二个阶段,那么我们可能遇到需求不明确的问题和不知如何技术实现的问题。Tasking 在 TDD 中如此重要,那我们改如何进行 tasking 呢?
TDD 中的 tasking
Tasking 是对业务需求进行拆解,将业务需求分解为有上下文、行为和结果的一个或多个任务的列表,并且任务列表中的每个任务是简单的、能快速实现的、有具体场景的。敏捷交付中,针对用户故事(User Story)的 tasking 和验收标准,目的都是对业务场景的细化和定义,验收标准更多的由业务方来定义,tasking 是开发人员来做的。我们提倡站在业务的视角进行 tasking,能够找到业务的边界,比如:手机号验证码登陆的业务需求,我们大体可以拆分成如下三个任务。
-
Given 手机号和验证码都正确
When 用户登录
Then 登录成功,跳转到首页
-
Given 手机号不存在
When 用户登录
Then 登录失败
-
Given 验证码错误
When 用户登录
Then 登录失败,登录页面验证码错误
很多人在实际项目中觉得 TDD 很难,一个重要的原因是他没有做 tasking,没有对复杂的问题进行分解,脑海里有大量的信息,认知比较混乱和复杂,不知道第一个测试从哪里开始。或者写着写着就不知道自己写到哪了,整个节奏是乱的,这样下来实施 TDD 是很有难度的。
Tasking 通过对需求的分析和拆解,将我们的关注点从混乱和复杂的问题聚焦在更小的问题,从而也更容易去解决,混乱和复杂的问题转化成繁杂的问题,繁杂的问题拆小之后变成了简单明显的小问题,有了对需求清晰准确的认知的人做 TDD 的难度会降低。
那么 tasking 要进行要什么颗粒度呢?
- 检查自己能否对分拆出来的任务的每种场景做一些实例化,比如验证码输错,假设正确的验证码是123456,那么错误的验证码就可以是654321
- 自己能否根据每一个任务场景进行评估,能否准确的评估出工作量的大小
总结
建议将 tasking 作为 TDD 的第一步,提高我们对需求的认知水平,让 TDD 更顺利。回到开篇的那个问题,那么保龄球的边缘用例应该在 tasking 的阶段来考虑,而不是在写测试和功能代码的时候。
文/Thoughtworks刘勇智
原文链接:https://insights.thoughtworks.cn/tdd-waterfall/
更多精彩洞见,请关注微信公众号Thoughtworks洞见。