落地TDD,需要知道的

字数 2736阅读 308
TDD

TDD是个好东西,推广时,需从Why/what/how来说服别人,结合最近看的一些资料,来说明一下问题:

1、单元测试的理由


《[高效程序员的45个习惯:敏捷开发修炼之道》中已经对于单元测试好处做了一些说明:

- 单元测试能及时提供反馈。你的代码会重复得到锻炼。若修改或者重构,作为防护网会检查是否破外了已有功能。快速得到反馈,并容易修复他。他是重构的基础。

- 单元测试让你的代码更健壮。测试帮助你全面的思考代码的行为,帮你联系正面,反面以及异常情况。

- 单元测试是有用的设计工具。有助于简单设计,注重实效。

- 单元测试是让你自信的后台。你测试代码,了解不同条件下的行为,面对压力找到自信。

- 单元测试解决问题的探测器。提供发现问题和解决问题的方法。

- 单元测试是可信的文档。 学API时,可以帮你提供精准和可靠的文档。是程序员之间的语言。

- 单元测试学习工具。帮你了解API行为。

2、TDD的风格


参考文章《TDD 真的能带来好设计么?》(原文:http://codurance.com/2015/05/12/does-tdd-lead-to-good-design/)中介绍TDD有两种主要的风格,它们在何时进行设计有着相当显著的区别。

2.1 古典派

古典派是由Kent Beck开创的原本风格,也被称为底特律式TDD

2.1.1 主要特点

- 设计在重构阶段发生。

- 一般来说测试是基于状态的测试。

- 在重构阶段,由测试驱动的单元可能会分化为多个类。

- 极少使用Mock技术,除非是要与现存系统隔离。

- 在编码前不进行设计方面的思索。设计完全是从代码中浮现出来的。

- 是避免过度设计的极佳方法。

- 基于状态的测试以及无前置设计,使得这种风格更容易理解和采用。

- 常常结合简单设计四规则一起使用。

- 当我们知道输入和期望的输出,但是不清楚实现可能是什么样的时候,很适合使用这种方式进行探索。

- 在我们无法借助行业专家和行业语言(比如数据转换,算法等)的情况下很有帮助。

2.1.2 问题

- 单纯为了测试暴露状态。

- 相比由外而内风格,重构阶段通常更大。后面会详细介绍由外而内风格。

- 当新的类在重构阶段浮现出类时,被测的单元就会超出一个类。如果我们单单看那个测试的话,这没什么问题,然而,当新的类浮现出来后,它就有了自己的生命。它会被程序的其他部分复用。当这个类演化时,可能会破坏其它不相关的测试,因为那些测试使用了它们实际的实现而不是mock对象。

- 经验不足的练习者往往会跳过重构,也就是设计改进的阶段,导致开发进程变为 红灯—绿灯—红灯—绿灯—...—红灯—绿灯—大重构。

- 由于探索式的特点,测试驱动下的类产生于“我想我们需要一个有这样接口的类”。可能无法和系统的其它部分很好地对接。

- 有可能会缓慢费事。我们常常一开始已经知道被测的类不应该有这么多的职责,古典派的建议是等待重构阶段,只在确实有必要时才抽出其它类。对于初学者来说这可能是个好建议,对更有经验的程序员纯粹是浪费时间。

2.2 由外而内

由外而内TDD,也被称作伦敦派或者mock式,是由一些XP实践先驱接受和发展出的风格。随后它启发产生了BDD。

2.2.1 主要特点

- 不同于经典派,由外及内TDD为我们如何着手测试驱动代码做出了规定:从外(接到外部输入的第一个类)到内(各个将会实现系统需要的一个单一特性的类)。

- 一般从一个验收测试开始,验收测试用来检验是否整个功能工作。验收测试也为实现进行了向导。

-验收测试的失败会告知我们功能还有某些部分没有实现的信息(数据没有返回,消息没有送入队列,数据没有存人数据库等等),从中我们可以开始进行单元测试。第一个被测的类负责接收外部的请求(比如一个控制器,队列监听器,事件接收器,组件入口等)。

- 鉴于我们知道我们不会在一个类里实现整个程序,我们会假定被测试类会需要一些合作类。然后我们在测试里验证被测类与其合作类之间的协作。

- 通过被测类需要调用合作类公开方法来完成的各种事,可以识别出合作类。合作类的名字和方法名应该来自于行业语言中的名词和动词。

- 当一个类被完全测试后,我们选取一个合作类(此时应该还没有进行实现),并采用与上一个类相同的方式,通过测试驱动它的行为。这就是我们叫它由外而内的原因:我们从靠近系统输入的地方(外部)通过不断识别合作类,逐步向程序内部推进。

- 设计起始于红灯阶段,也就是开始写测试时。

- 测试测的是行为和协作,而非状态。

- 在重构阶段对设计进行完善。

- 每个合作类以及它的公开方法都是为了给已有的客户类提供服务的,这使得代码可读性很高。

- 相比经典式方法,由外而内式的重构阶段要小得多。

- 提高了封装性,因为不需要仅仅为了测试而暴露状态。

- 更符合“tell, don't ask”设计方法。

- 更符合面向对象编程的原本理念:测试是关于对象间发送的消息,而非检测对象的状态。

- 适用于商业应用,从user story和验收条件中可以获得名词和动词。(作为类名和方法名)。

2.2.2 问题

- 对于初学者来说很难接受,因为需要更高层次的设计能力。

- 开发者从代码中无法得到反馈来创建合作类。他们需要通过写测试来使合作类显现出来。

- 不成熟的创建合作类有可能导致过度设计。

- 不适用于探索式的工作,以及不特定于user story的行为(数据转换,算法等)。

- 设计能力糟糕的话可能会导致mock对象爆炸式的激增。

- 基于行为的测试要比基于状态的测试难写。

- 在写测试的时候需要具备DDD(领域驱动设计)以及其他设计技能,包括简单设计四规则。

2.3 小结

虽然两者各有优缺点,考虑维护性和效益问题,一般考虑从外而内,选择接口稳定,并且具有业务含义的用力进行测试。

3、落地TDD遇到的问题


推广TDD,一般阻力都比较大,主要有以下方面:

3.1 质量意识

传统的软件研发模式,对于软件质量,常常偏重于软件的外部质量。检查开发人员的的工作效益或者质量,主要是测试人员发现的缺陷数。这种模式下,形成的惯性思维认为开发人员不适合做测试,主要有测试人员进行质量保证。

推行TDD,以为不仅仅要关注外部质量,又要关注内部质量。当有开发人员进行TDD时,因为代码的可测试性问题,会导致单元测试举步为艰,这也成为阻力。如果代码的内部质量低下,但在足够覆盖率的保护下,使得重构变得更为简单。

 3.2 业务分析和拆解能力

在TDD时,如果按照标准的敏捷模式运行,需要有完整的故事和AC。如果BA角色缺失或者PO对于AC描述不清,在编写TDD的用例(To-do list)时,将成为问题。业务的分解需要分为三个层次,即业务价值—>业务功能—>业务实现。

 3.3 测试先行的开发习惯

养成一个习惯需要二十一天,改变一个习惯需要六十天。 要让开发人员改变习惯需要一个过程。记得自己改变的过程中,强制自己通过截屏方式记录每个过程。一周下来,基本可以做到测试先行和三步提交原则。

3.4 Clean Code和重构

TDD的核心是红——绿——蓝。蓝色意味着重构。重构是TDD非常重要的一环,它直接关系到TDD开发出来的代码质量。没有好的重构能力,TDD就会有缺失。实施TDD,需要开发人员具备Clean Code和重构的能力。

3.5 业务测试框架

良好的测试框架,使用测试用例编写和测试数据管理变得容易。在测试框架的付出是需要值得。自己的团队在推广TDD,也是得意于良好的测试用例,从而保证4个月时间,测试用例从200增长到1000以上,覆盖率从20%达到39%以上。