【敏捷开发】测试驱动开发(TDD)

测试驱动开发(Test-Driven Development,简称TDD)是敏捷开发模式中的一项核心实践和技术,也是一种设计方法论。TDD有别于以往的“先编码,后测试”的开发模式,要求在设计与编码之前,先编写测试脚本或设计测试用例。

一、TDD概念

敏捷开发大师Kent Beck在1996年提出了极限编程(Extreme Programming,简称XP)。TDD是其四个核心实践之一。【注:参见【架构基础】简单设计原则】

TDD在敏捷开发模式中被称之为“测试优先的编程(test-first programming)”,而在IBM Ration统一过程(Rational Unified Process,RUP)中被称为“测试优先的设计(test-first design)”。所有这些,都在强调“测试先行”,使得开发人员对所做的设计或所写的代码有足够的信心,同时也有保障地进行设计或代码的快速重构,有利于快速迭代、持续交付。重构的前提就是测试就绪(testing is ready),在这样的前提下,重构的风险就很低,否则就会存在比较高的风险。

TDD具体实施过程,可以嵌入到两个层次,如下图所示:

  1. 狭义TDD:在代码层次,在编码之前写测试脚本,可以称为单元测试驱动开发(Unit Test Driven Development,简称UTDD)。
  2. 广义TDD:在业务层次,在需求分析时,就确定需求的验收标准,即验收测试驱动开发(Acceptance Test Driven Development,简称ATDD)。

【敏捷开发】测试驱动开发(TDD)_第1张图片

 

二、UTDD

先来谈谈UTDD,基本流程如下图所示。

  1. 红(RED):在做某个新需求时,先不着急编写功能代码,而是把需求涉及的场景、约束等考虑清楚,先写好测试用例。然后执行单元测试代码,结果自然是不通过(失败)。
  2. 绿(GREEN):利用单元测试的失败反馈,查明功能代码未通过测试用例的原因,针对性地添加或修改代码,直到测试用例通过。
  3. 重构(REFACTOR):在所有单元测试用例执行成功的基本前提下,识别代码或设计上的坏味道,进行重构。通过现有的单元测试用例,来保证重构的正确性。

【敏捷开发】测试驱动开发(TDD)_第2张图片

 

UTDD的三条规则

  1. 规则1 除非是为了使得一条失败的unit test通过,否则不允许编写任何功能代码。

违反第1条,先编写了功能代码,那这段代码是为了实现什么需求呢?怎么确保它真的实现了呢?

  1. 规则2 在一个单元测试中,只允许编写恰好能够导致失败的测试代码。

违反第2条,写了多个失败的测试用例,如果长时间不能通过测试,会增加开发者的压力。另外,测试可能会被重构,这时会增加测试的修改成本。

  1. 规则3 只允许编写恰好能够使一条失败的unit test通过的功能代码。

违反第3条,功能代码实现了超出当前测试的功能,那么这部分代码就缺少测试的防护,不确定是否正确,需要额外增加手工测试。可能这是不存在的需求,那就凭空增加了代码的复杂性。如果是现实存在的需求,那后面的测试用例写出来就会直接通过,破坏了UTDD的节奏感。

UTDD从根本上改变了开发人员(Developer,简称DEV)的思维方式,DEV不能再像过去那样随意地写代码,要求所写的每行代码都是有效的代码,写完所有的代码就意味着真正完成了开发任务。而在此之前,所谓的代码写完了,实际上只是完成了一半的工作,因为单元测试还未执行,可能会存在许多缺陷。

UTDD有力地促进DEV去思考需求的应用场景、异常处理或约束,写出更加完善的功能代码。其次,UTDD确保了测试的独立性。如果先写功能代码,再进行测试,容易受到实现思维的影响。多数情况下,DEV自测时存在两大障碍:思维障碍和心理障碍,前者会导致DEV无法保证测试的客观性和全面性;后者会导致DEV对自己的代码不愿深究,即使发现了一些疑问,也很可能会适可而止。

最后,UTDD确保了所有功能代码的可测试性,彻底地保证了代码的微观质量,最终实现了可测试的系统。

三、ATDD

通常,在一个大型项目中,推行UTDD比较困难,而在业务层面推行ATDD,即在设计与开发之前,明确需求特性的验收标准,则相对比较容易推广实施。

在敏捷开发模式中,由需求拆分出来的用户故事(User Story,简称US),一般描述简单,不具备可测试性。

举个例子

开发一个在线旅游APP,提供交通、酒店、景点门票等预订服务,有一个最基本的US:作为一名旅游用户,想通过一次操作,快速删除事先预订的订单包(含机票、酒店和门票)。

对于这种US,如果不附加验收标准,DEV实现起来很容易:在数据库的某个表中删除一条记录,在其他关联表上修改相应的标志位即可。但实际业务不会如此简单,订单说取消就取消?不需要有一个取消的时间提前量?取消一定成功吗?是否要收取手续费?是否需要线下处理时间?是否需要通知用户?采用何种方式通知用户取消成功或失败?

回答上述问题,就需要增加“验收标准”,如:

  1. 订单取消之前,需要提醒用户再次确认
  2. 提示用户需要提前24个小时取消
  3. 订单取消需要4个小时处理时间,才能确定取消成功与否
  4. 订单取消需要收取总金额10%的手续费
  5. 不管取消成功与否,采用邮件和短信双重通知
  6. 用户事后可以查询订单取消的记录
  7. 需要保留客户和APP双向操作记录日志

如此,US就具备了可测试性,DEV也更明白如何去实现,实现的结果和产品经理的期望更容易达成一致。

从ATDD演化出一种可具体落地的开发模式就是行为驱动开发(Behavior Driven Development,简称BDD)。BDD最初是由Dan North在2003年命名,它包括验收测试、客户测试驱动等XP实践。作为对TDD的回应,主要是从用户的需求出发,强调系统行为。BDD将验收标准进一步明确化,可看作是ATDD的实例化,即列出US涉及的应用场景并表达为Given-When-Then范式:

  1. Given:给定什么上下文/条件 AND/OR 其他条件
  2. When:当什么事件被触发
  3. Then:产生什么结果 AND/OR 其他结果

BDD再往前推进一步,就是需求实例化(Requirements By Example,简称RBE),更加明确需求的具体表现。需求描述越明确,需求干系人(用户、产品经理、DEV与TSE等)之间的理解就越趋近一致,不容易产生偏差或误解,有利于开发和测试的工作。基于RBE,DEV编写需求的功能代码,TSE可以独立编写测试代码,产品经理的工作也会变得轻松,不需要太多的解释,不需要回答开发与测试的各种问题。

  1. 从需求角度看,BDD和需求实例化比较彻底地明确需求,统一用户、产品经理、DEV与TSE等人员的认知,让大家在同一个层面上沟通,使得研发工作更高效。
  2. 从测试角度看,需求即测试,产品的需求就是测试的需求,需求可以被执行,即一步到位,将需求变为自动化测试脚本,开发出来的功能特性随时可以被验证。

TDD一改以往的破坏性测试的思维方式,提倡“测试先行”,更符合“缺陷预防”的思想。这样一来,开发的思维方式发生了很大的变化,编写出高质量的功能代码去通过这些测试,在进行每一项设计、编写每一行代码时,都要想想用户的真实需求、应用场景和一些例外情况等,确保实现的功能特性符合预期,并具有健壮性。测试也从以前的破坏性的方法,转移到一种建设性的方法中来。在这种积极心态的影响下,DEV的工作效率和产品代码的质量都会显著地提高,真正实现“质量是内建的(Quality is built in)”的目标。

四、TDD的价值

TDD从业务层次(需求分析、软件设计),促进做正确的事,即设计出符合用户需求包括功能需求与非功能需求的软件特性。

TDD从代码层次(软件开发、软件测试),促进正确地做事,即开发出与设计完全一致的软件功能。

TDD充分体现了“测试先行,小步迭代,快速反馈”的敏捷思想,从微观代码到宏观系统,均践行着“要想跑得快,先要跑得稳”的目标。

你可能感兴趣的:(架构基础,敏捷流程,TDD,软件工程)