测试驱动开发(Test-Driven Development,简称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具体实施过程,可以嵌入到两个层次,如下图所示:
先来谈谈UTDD,基本流程如下图所示。
UTDD的三条规则
违反第1条,先编写了功能代码,那这段代码是为了实现什么需求呢?怎么确保它真的实现了呢?
违反第2条,写了多个失败的测试用例,如果长时间不能通过测试,会增加开发者的压力。另外,测试可能会被重构,这时会增加测试的修改成本。
违反第3条,功能代码实现了超出当前测试的功能,那么这部分代码就缺少测试的防护,不确定是否正确,需要额外增加手工测试。可能这是不存在的需求,那就凭空增加了代码的复杂性。如果是现实存在的需求,那后面的测试用例写出来就会直接通过,破坏了UTDD的节奏感。
UTDD从根本上改变了开发人员(Developer,简称DEV)的思维方式,DEV不能再像过去那样随意地写代码,要求所写的每行代码都是有效的代码,写完所有的代码就意味着真正完成了开发任务。而在此之前,所谓的代码写完了,实际上只是完成了一半的工作,因为单元测试还未执行,可能会存在许多缺陷。
UTDD有力地促进DEV去思考需求的应用场景、异常处理或约束,写出更加完善的功能代码。其次,UTDD确保了测试的独立性。如果先写功能代码,再进行测试,容易受到实现思维的影响。多数情况下,DEV自测时存在两大障碍:思维障碍和心理障碍,前者会导致DEV无法保证测试的客观性和全面性;后者会导致DEV对自己的代码不愿深究,即使发现了一些疑问,也很可能会适可而止。
最后,UTDD确保了所有功能代码的可测试性,彻底地保证了代码的微观质量,最终实现了可测试的系统。
通常,在一个大型项目中,推行UTDD比较困难,而在业务层面推行ATDD,即在设计与开发之前,明确需求特性的验收标准,则相对比较容易推广实施。
在敏捷开发模式中,由需求拆分出来的用户故事(User Story,简称US),一般描述简单,不具备可测试性。
举个例子
开发一个在线旅游APP,提供交通、酒店、景点门票等预订服务,有一个最基本的US:作为一名旅游用户,想通过一次操作,快速删除事先预订的订单包(含机票、酒店和门票)。
对于这种US,如果不附加验收标准,DEV实现起来很容易:在数据库的某个表中删除一条记录,在其他关联表上修改相应的标志位即可。但实际业务不会如此简单,订单说取消就取消?不需要有一个取消的时间提前量?取消一定成功吗?是否要收取手续费?是否需要线下处理时间?是否需要通知用户?采用何种方式通知用户取消成功或失败?
回答上述问题,就需要增加“验收标准”,如:
如此,US就具备了可测试性,DEV也更明白如何去实现,实现的结果和产品经理的期望更容易达成一致。
从ATDD演化出一种可具体落地的开发模式就是行为驱动开发(Behavior Driven Development,简称BDD)。BDD最初是由Dan North在2003年命名,它包括验收测试、客户测试驱动等XP实践。作为对TDD的回应,主要是从用户的需求出发,强调系统行为。BDD将验收标准进一步明确化,可看作是ATDD的实例化,即列出US涉及的应用场景并表达为Given-When-Then范式:
BDD再往前推进一步,就是需求实例化(Requirements By Example,简称RBE),更加明确需求的具体表现。需求描述越明确,需求干系人(用户、产品经理、DEV与TSE等)之间的理解就越趋近一致,不容易产生偏差或误解,有利于开发和测试的工作。基于RBE,DEV编写需求的功能代码,TSE可以独立编写测试代码,产品经理的工作也会变得轻松,不需要太多的解释,不需要回答开发与测试的各种问题。
TDD一改以往的破坏性测试的思维方式,提倡“测试先行”,更符合“缺陷预防”的思想。这样一来,开发的思维方式发生了很大的变化,编写出高质量的功能代码去通过这些测试,在进行每一项设计、编写每一行代码时,都要想想用户的真实需求、应用场景和一些例外情况等,确保实现的功能特性符合预期,并具有健壮性。测试也从以前的破坏性的方法,转移到一种建设性的方法中来。在这种积极心态的影响下,DEV的工作效率和产品代码的质量都会显著地提高,真正实现“质量是内建的(Quality is built in)”的目标。
TDD从业务层次(需求分析、软件设计),促进做正确的事,即设计出符合用户需求包括功能需求与非功能需求的软件特性。
TDD从代码层次(软件开发、软件测试),促进正确地做事,即开发出与设计完全一致的软件功能。
TDD充分体现了“测试先行,小步迭代,快速反馈”的敏捷思想,从微观代码到宏观系统,均践行着“要想跑得快,先要跑得稳”的目标。