微信群分享第三期 TDD by 丁辉

线上活动 · hkliya · 于 发布 · 最后由 hkliya回复 · 820 次阅读
1

日期:2016-01-13
主题:《TDD 的本质不是 TDD》
简介:

  • TDD是什么
  • TDD的出处
  • 为什么引入TDD
  • 如何开展TDD
  • TDD的收益
  • TDD 的Keys

讲师:丁辉,中兴通讯敏捷管理和技术教练,12年软件开发经验,8年项目管理和流程改进经验,指导并参多个团队由传统研发模式向敏捷研发模式转型,同时对如何提升员工代码设计能力和提升代码内在质量等方面较多解决思路。

特别感谢「木子李」对活动的主持和演讲内容的整理。以下为「木子李」根据语音整理的文字内容。

先自爆一张照片以示诚意!

大家看了照片,也领了红包,那我们的课就正式开始了。
谢谢大家牺牲晚上的时间来一起研讨TDD,我看到了大家对技术的激情和热情。
我今天分享的话题是TDD,当然起了一个比较炫目的题目《TDD的本质不是TDD》。

在敏捷推进的过程中,一般认为有三大难点。

  • 第一大难点就是故事拆分,我们的故事又要纵拆,又要拆小。纵拆就意味着横跨整个端到端的流程,拆小意味着尽量要短。而且纵拆和拆小本身相互就是矛盾的,所以觉得敏捷推进第一难点就是拆分。
  • 第二大难点,就是我们平时说的团队建设。大家想一想,我们大部分都不是企业的股东,那我们有什么力量去组织团队呢?
  • 第三大难点,就是我们经常说的TDD。要改变很多我们传统研发的思维方式和观念,同时又要牵涉到软件设计方面的问题本质复杂度而这又是系统工程层面的东西。所以说,它本身的难度也是比较大的。 TDD就是Too Difficult to Do,太难了,以至于我们没法做。实际上,业内以前认为是这三个单词的缩写,Test Driven Development。但是随着时间的发展以及TDD的开展,TDD的本质不再是TDD,更多的是这三个单词,Test Driven Design,即测试驱动设计。因为TDD本质过程就是要贯穿从需求分析、设计、编码、测试、整个研发过程,所以现在提的更多的或者主流观念都是Test Driven Design。 那我们接下来要谈TDD,我们就要谈它的前世和今生,前世就是TDD从哪里来?

前世

大家看一下上面的图,这张图对于熟悉敏捷技术实现的同学可能比较熟悉。其实敏捷流派比较多,目前有十多种。现在比较主流的就是SCRUM + XP 。SCRUM主要用于管理实践,有阶段的定义和管理的支撑,技术实现主要是XP - 极限编程。这个XP极限编程,主要由三个环组成。三个环里面共有13个实践 ,因为中间环的隐喻实践使用不是很多,所以给出了常用的12个实践。大家看,从外往里面看这个洋葱环,最外面的环叫做组织实践环,这个是力度最大实践环。是由四个实践组成,当然四个实践彼此是地位不相当的。其中有一种叫做核心实践,其它叫做拉动实践。核心实践叫做,Small Release。只有做到Small Release,才能快速实现商业价值、快速得到客户反馈,其它的测试、完整团队啊都是为了Small Release服务的。

那第二层环,也就是中间层叫做团队实践环,力度相对于组织环小了一层。组织环是相对于产品层级的,而它是Team层级的。实践环也是有核心实践和拉动实践组成,大家应该都能猜到这一环的核心实践就是稳定节奏。只有团队保持稳定的节奏,才能使团队风险降到最低,脉冲和毛刺(指交付速度大幅波动)都会带来极大不确定性。其它如持续集成、代码规范都是为了支撑稳定的节奏而服务的。

好了,下面让我们把目光集中到最内层的实践环,也就是洋葱的「芯」,它也由四种实践组成:简单设计、结对编程、TDD、重构。大家猜一下,哪个是核心实践?好,同学应该都能答对,这一环的核心实践就是「简单设计」。它指对于要解决问题域的本质复杂度映射,比如我们要做一款软件来解决某个问题,本身问题是有复杂度的,现假设问题复杂度为100。如果软件要解决这个问题,很自然软件的复杂度就要大于等于100才能hold得住这个问题。而这个软件又是人写的,人要理解和维护这个软件。所以人脑的思维复杂度又要大于等于软件的复杂度。这样,如果我们一个设计能够无限趋近于问题本质复杂度。使我们维护、理解软件的成本最小,这种设计就是简单设计,它是我们追求的目标。我们无论是做重构,做重构我们可以使用结对编程和TDD方式,这都是为了使解法逼近本质问题复杂度,也就是简单设计。TDD就是我们图中方块的地方,它是我们实现简单设计的手段。
这就是TDD的前世,也就是TDD从哪来的。

下面,讲讲TDD的粒度。
在敏捷中所有的测试,单元测试、功能测试也好,都是黑盒的测试,只不过单元粒度有大有小。
按照粒度的大小来分,一般来说TDD有以下几种粒度组成。

UT

这个粒度是UT,这里的UT要和传统的UT相区分,在敏捷中所有的UT其实都是默认是黑盒UT,是功能单元的UT,只不过功能粒度比较小,它本身非常内聚,麻雀虽小五脏俱全,都是基于对外功能接口上的,所以一定都是黑盒的,基于接口的测试。

FT

粒度最大一层我们一般把它叫做FT,Function Test。针对前面的UT来说,到这里是指模块级了,粒度更大一些,是把N个UT串起来。

BDD

现在引入BDD,其实从本质来说是一种TDD展现形式,只不过它的粒度更大,从用户行为的角度来进行测试。

ATDD

是一种验收测试,系统需求级别的测试。TDD本身是分层的,UT、BDD这些本质来说没有差异,都是对接口的测试对功能的映射从而驱动开发,我们可以叫做XDD。

TDD和传统UT之间的差别

下面我们讲讲TDD和传统UT之间的差别,传统UT,最大的问题一般都是事后编写, 这样我的用例会迁就我的代码。迁就的意思就是,生产代码已经写好或者在某种压力上针对生产代码做一些白盒的单元测试,因为代码已经产生,也不可能废除或者删除掉。只能用例迁就代码,如果代码耦合、灰色,用例自然也就很耦合和、灰色。那这种迁就化会造成测试白盒化,白盒测试最大的要命问题就是造成用例不稳定!我们都知道,从稳定性角度来说,需求和实现相比,需求相对稳定,实现相对变化就比较大。比如换实现方法或者性能不达标要重构。针对稳定性来说,一般需求相对稳定,那测试白盒化需要我们把用例写在实现上,代码有多少分支就写成用例。实现相对需求又不稳定,所以实现一改变就会导致用例变化,从而用例很难维护。

做过传统UT的团队可能都会有这种体会,当年我所在的团队也做过传统UT,用例太容易变化。以至于我不得不花额外的人力维护用例,导致很多团队坚持不下来。用例白盒化造成第二个问题就是需求脱节,我们知道一个需求它被实现之后,是通过很多代码逻辑组合起来的,如果用例写在代码逻辑上,就很难和明确的需求相对应,造成用例和需求脱节。一旦某个用例不通过,很难和明确的需求对应起来,说不清哪个需求受到影响,这也是用例很难维护的原因之一。第三,测试白盒化造成用例性价比相对不高,我们也做过试验,如果要想达到60%的分支覆盖率。采用完全的白盒化方式,用例的代码行数和生产代码的行数要达到2:1左右的比例。大家看为了达到60%分支覆盖率,要付出两倍的测试代码的代价,这个性价比是相对不高的。很多开发人员为什么对UT有反感和抵触也是这个原因造成的。因为我除了要维护生产代码以外,还要维护测试代码。采用TDD方式以后,把用例写在需求上、接口上、场景上,通过用例把一个个白盒逻辑串起来,就像一个珍珠项链有一条线把所有的珍珠串起来。这样同样达到分支60%的覆盖率,用例的代码数量和生产代码相比大幅度下降,甚至几倍的数量级。所以,白盒用例往往会造成用例非常复杂,因为是针对实现嘛。我曾经自己做过一些码流,就是二进制码流。针对协议栈码流的单元测试,构造用例就要构造成二进制,如果没有适当的第三方工具和自研工具,码流构造会非常复杂而且很难看懂和难以维护。跑出来的用例也很难定位,它非常复杂、耦合和晦涩。

所以以上种种往往用例成了一个重构的障碍。人心里都是这样,我们经常讲沉没成本,就是说投出努力后付出再也收不回来了,就会一般很难放弃。这个白盒用例也是这样,我们花了这么的代价,刚才上面说的为了达到60%的分支覆盖率测试代码和生产代码2:1的比例,这么大的代价已经付出了。可重构,就意味着我们不改变系统外在的行为而要改变内在的实现。那我的用例就是写在内部实现上的,所以我大量的用例跑不通过。如果放弃这些已经完成的用例,自然我很心痛。所以我不愿意去重构。
刚才我们讲TDD和传统的白盒用例的差别。当然了,由于白盒测试这么多问题,也是促进我们进一步开展TDD的动力之一。那下面就和大家分享一下TDD怎么做,是不是Too Difficult to Do?其实它还是有一定的方法和脉络的。那TDD的本质,我理解就是需求分析,这也对应我本次的分享,TDD的本质不是TDD而是需求分析。

所以,我们在做TDD之前,首先拿到需求,之后对需求进行分析,分析过程就是把需求按照故事来拆分,然后故事按照场景进行分析,然后每个场景实例化。比如我有一个接口,接口有返回值,返回值具体实例化成0和1。这样需求就会被拆解开来。需求拆解开来之后,针对需求的每个故事,故事的每个场景来写用例。这个时候有两种方法,第一种,先设计出接口然后写用例。另一种先写用例然后再定义接口。大家看一下,觉得哪种方法更合适一点 ?答案这样的,我们推崇针对拆分好的故事和场景先写用例,先用这个用例决定接口什么样子,然后再把接口定义出来。用例针对接口而写,而接口在某个场景下就是代表需求的,从而我的用例就是可以代表需求的 。

用例编写

通过编写用例再把接口定义出来,这种方式我们做过实际的测试 这是一个统计数据。如果分支覆盖率达到一定目标,和白盒测试相比,测试代码和生产代码会下降三到四倍。所以说针对需求开发用例性价比会提升。

TDD的做法就是六个字,我们常说的六字真言:红色、绿色、蓝色。红色的意思就是说我把这个需求分析完之后,先把需求进行实现。主要是为了验证接口设计对不对,并没有做具体实现,这个时候编译通过后一跑就是红色。第二个快速实现,就是把很多复杂的策略写死,比如直接return 0或者return 1,绕过去复杂的策略。这个时候,它一跑结果就是绿色。那有同学说了实现这种绿色有什么意义呢?其实一次贯穿的用例至少代表一种业务场景,可以去验证编译和语义环境。第三个蓝色,就是说把用例快速实现然后果断重构,重构过程看看是不是命名充分表达用户语义,横向排版和纵向排版是否整齐,去掉多余的空格和换行使得语义更紧凑,检查逻辑是否可以更顺畅,把所有的重复都消掉,使我们的代码变得海水天空一样湛蓝,这个我们叫做蓝色。然后,变蓝后我再去实现下一个场景,然后再去重复这个过程,红色、绿色、蓝色。那有的同学可能会问,为什么我不能一步把它变成绿色或者一步变成蓝色?为什么还要有一个变红的过程那?这个地方,我们先埋下一个伏笔,后面我们会重点讲述这个问题。

TDD特点

接下来我们谈一下TDD的特点,那第一个特点就是驱动开发。
为什么叫做驱动开发,驱动开发有鞭策、促使、推动的作用,其实是一个主动词。为什么叫它主动词,因为我们先对需求进行拆分,拆分成一个故事然后按照场景拆分,然后针对场景写测试用例,针对测试用例使用接口定义接口原型。这个原型包括接口本身,包括接口事前条件就是说满足调用条件,然后包括事后条件。这就是接口的原型事物的三要素。最后才是针对接口的一个实现并跑通用例,所以大家看所有的过程都是需求驱动开发,而不是传统的对需求还不是很了解的情况就去开发,然后再去测试,这有一点开发驱动需求的感觉。这就是TDD里面测试驱动开发的第一层含义。
第二层含义,就是说我们不仅需求能驱动开发而且要刚刚好,Just In Time 。那什么叫做刚刚好,就是说用例设计完后,代码去实现,跑通用例后用覆盖率工具去查看,发现所有的代码都对用例有贡献。在我场景拆分足够细致详细的情况,我们发现某些代码对用例没有贡献,就认为这个代码是冗余代码。所以第二层含义就是不仅驱动,且实现的刚刚好。
TDD的第二个特点就是改善设计。我们经常看到网上有一副漫画,客户眼中的程序员 都是外星人的样子,鼻子长在脑袋上,皮肤是绿色,说的外星语,和她很难沟通,这就是客角度他眼中的程序员的样子。同样,程序员眼中的客户是什么样子呢?从程序员眼中看客户,都是穿着兽皮裙扛着大木棒头发乱糟糟的野蛮人形象,毫不讲道理,程序员提供的接口,他随意调用接口,粗暴的调用。为什么会有这种现象?因为大家都站在自己角度看问题。TDD要求程序员先看需求再去实现,强制程序员先把屁股挪到客户那里。从客户的角度来看待接口设计的问题,这样往往能发现很多深层次设计问题。比如你先写用例然后定义接口,很容易发现接口过度依赖点的问题。也叫作依赖过多问题,比如说我有一个三角形的类,有一个接口可以get三角形的底,另一个接口可以get三角形的高,然后客户get到三角形的底和高后,再用底乘以高除二计算三角形的面积。大家猜猜这个计算三角形面积的调用,对我的三角形类有几个依赖点。其实是三个依赖点,除了get底和get高, 还用到一个三角形知识:底乘高除二等于面积。所以说这个接口设计不合理,存在过度依赖的问题。所以我们希望把这个接口进行修改,增加一个计算面积的接口 - triangleArea。与前面相比有什么特点,首先接口的数量减少了。我们说到接口本身背后的知识,如果一个模块依赖另一个模块过多,那另一个模块变动,则该模块变动的概率就会增大,造成波及影响。同理,第二个那个针对get底、get高,计算三角形面积来说,获取面积接口triangleArea更稳定。比如说计算三角形不再是底乘高除二,而是两个临边乘夹角除二。则后面这个接口就不用变化,所以这个接口更稳定。综上,从用户角度来看,这个接口是否造成依赖过度。

第二个,从客户角度来看接口,还容易看出接口的功能是不是单一,接口是不是易错。比如说,我们经常讲的c语言stringcopy函数,一定是destination放在前面source放在后面,如果设计的接口把source放在前面而destination放在后面,对于一个熟悉c的接口人员就非常可能调用错误。第三个,看看接口是不是有第三方依赖。我们碰到很多的接口用起来非常不爽,因为里面有第三方依赖,还要自己去管理。没有对必要的第三方依赖进行抽取,这个也有利于审视你的接口。所以TDD对改善设计是非常有利的,做TDD确实有一些思考,比如说设计上更本质更背后的一些非常有意义的东西。

第三个特点就是可以快速反馈。在TDD中,所有的用例都可以积累起来而且还是自动化的,所以有问题可以及时发现快速反馈,可以快速形成一个保护网。TDD我们刚才讲了驱动开发,不仅实现需求而且仅仅实现需求,可以消除过度开发和过度设计。快速反馈的另一个作用可以激发你进一步做一件事,比如说在游戏里面打怪或者做某一件事可以马上获得经验值或者宝物,这个可以刺激人的多巴胺神经,让你继续去打怪或者做某一件事情。其他还有很多这种特点,比如说提升用例的表达力啊,这里就不一一累述了。

然后第四点,TDD可以起到一个需求文档的作用,对场景描述表达力非常强。经常使用三方工具看说明文档不一定能懂,但是看用例,把用例抄一遍一般都可以直接运行的。所以说,TDD非常强大抽取非常好的时候,确实可以起到一个文档的作用。
第五点就是可以生成一个保护网,可以快速对基础功能进行回归,一般TDD都是UT、FT这样的,运行时间比较短反馈速度比较快,起到一种保护好的作用。

下面我们分享一下TDD里面的关键点,就是TDD有一些步骤是很关键的,这一步我们是不能跳开的。

小步快跑

TDD也有这种作用,我们叫做Small win。我们用例红色,然后快速实现绿了,然后快速重构,又绿了。心里多么刺激,然后不停的小步快跑,然后不停的反馈通过这种刺激去做TDD。

测试即文档

独立性

做好这个独立性,做好隔离就很容易就能定位代码的问题。

以上就是分享就是我们做好TDD的关键点,下面我们看一下做好TDD都有哪些挑战。

第一个叫做Small Step,小步快跑,因为TDD本身要做好前面讲要有一个Small win。通过小步修改,小步提交、小步遍历,从而获得小步胜利的感觉,会获得一种节奏感。大家是否跑过马拉松,我本身在南京包括去年都有参加,跑马拉松最重要的就是节奏感,一旦有节奏感就不容易累。TDD的好处就是获得这种小步快跑的节奏感。
另一个提倡No debug,就是不提倡debug。就是通过这种小步快跑上一步是绿的,又提交代码又是绿的。如果说某一个小步变红了,因为我的步子小嘛,我就很容易看出问题,这样我也不需要debug。如果看了半天,实在发现不了问题,有可能两种原因。第一你的代码太复杂;第二个就是你的代码跨度太大了;步子大怎么了,容易扯着*,所以我们一定讲究Small step。如果在步子很小的时候,实在是通过眼睛走查看不出问题,那我们就回退,回到上一个阶段,重新一小步一小步走下来。

TDD第二个关键点就是要做到依赖隔离,因为我们知道很多的代码跑起来要依赖外部的环境、配置文件、甚至服务,才能跑起来。做TDD时候我们希望快速反馈,所以外部的依赖我们要拨开。这也是TDD的难点之一,我们一般可以通过Mock/stub方法来进行一些依赖的隔离。由对这个hardcode硬代码依赖变成抽象接口数据类型这种方式的依赖,由依赖细节到依赖稳定抽象。然后做测试,通过依赖追踪注入到到生成代码中,这样我的用例就可以跑起来了。
下面一个关键点是黑盒,用例一定要写在接口上。接口暴漏的行为,就是我用例分别实现的场景。一旦用例跑不过,我能很快知道是哪个功能受到影响。

第四个就是独立性,用例独立性。一个用例跑不过之后我可以很清晰的知道是用例的问题还是第三方依赖问题。这也是刚才上面为什么讲要做依赖隔离。很多时候系统上线出问题后,我们很难界定是第三方问题还是自己代码问题 ,有可能就是第三方问题,但是错误实在代码中,所以说很难界定到底责任实在哪。如果我的代码都跑过了,上线后有问题那,那80%可以确定是第三方依赖问题。

最后一个要点叫做非侵入,就是我设计用例或者设计接口的时候,不能为了测试而增加分支或者编译条件。这个时候会对生产代码造成一定的伤害,我们也吃过很多这方面的亏。所以一定通过依赖隔离的方式,把测试分离隔离开,而坚决不采用这种伤害的方式。

下面留三个小问题,同学们自己思考一下!
第一个问题,是否所有代码都需要TDD?
第二个问题,TDD是否会降低效率?
第三个问题,TDD是否对团队技能有要求?

下面是课堂答疑题:

问题一

采用BDD的测试框架,还是xUnit的框架,更适合做TDD?

回答一

在coding层面,用xUnit可以快速获得反馈,启动small step和small win的作用;在特性和需求方面,用bdd或atdd,获得就需求的反馈,结合起来

问题二

您好,请问对与旧系统的维护使用TDD有哪些建议呢?

回答二

新增功能和bug修复可以采用tdd的方式,对3方遗留或旧系统遗留可以采用依赖隔离的方式。让开发人员感受到节奏感,然后逐步扩大tdd的范围

问题三

如何在团队内推动tdd,并使其长期有效,有没有好的经验分享?

回答三

管理上,采用吸引的方式,邀请tdd熟手经常插花和生手结对,帮传带;技术上,开发TDD框架,进行依赖隔离,让TDD很容易开发。
Tdd是一种软件开发的方法,软件设计能力是系统工程论的范畴。可以在不同的设计能力做能力想匹配的tdd,随着tdd的开展,又可以促进软件设计能力的提升。

问题四

你工作过程中有多少是采用TDD模式开发?实现推动时需要哪些层面的支持?

回答四

自己带的团队都是采用tdd开发,我当时每周抽2天每天2个小时找不同人结对,以点带面,形成氛围。


「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」和「习惯」以帮助程序员更加快乐高效地编程。
共收到 4 条回复
30
lvjian700 · #1 ·

Cool ! 期待分享...
最近也想写一篇关于 TDD 的内容。 基于 RSpec 按照 Mockist 方式,自上而下的 TDD。

1
hkliya · #2 ·

#1楼 @lvjian700 期待,我们可以来尝试视频直播。

30
lvjian700 · #3 ·

#2楼 @hkliya cool ! 视频很赞。之前在组里 pair 的时候搞过,可惜没录视频。
30 分钟边讲边TDD,做 OOCamp 中的出租车计价问题。

1
hkliya · #4 ·

#3楼 @lvjian700 我们可以约个时间先 Pair 一下,交流一下。

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。