TDD 的本质不是 TDD
关于作者 丁辉 日期:2016-01-13
大家看了照片,也领了红包,那我们的课就正式开始了。 在敏捷推进的过程中,一般认为有三大难点。
前世大家看一下上面的图,这张图对于熟悉敏捷技术实现的同学可能比较熟悉。其实敏捷流派比较多,目前有十多种。现在比较主流的就是SCRUM + XP 。SCRUM主要用于管理实践,有阶段的定义和管理的支撑,技术实现主要是XP - 极限编程。这个XP极限编程,主要由三个环组成。三个环里面共有13个实践 ,因为中间环的隐喻实践使用不是很多,所以给出了常用的12个实践。大家看,从外往里面看这个洋葱环,最外面的环叫做组织实践环,这个是力度最大实践环。是由四个实践组成,当然四个实践彼此是地位不相当的。其中有一种叫做核心实践,其它叫做拉动实践。核心实践叫做,Small Release。只有做到Small Release,才能快速实现商业价值、快速得到客户反馈,其它的测试、完整团队啊都是为了Small Release服务的。 那第二层环,也就是中间层叫做团队实践环,力度相对于组织环小了一层。组织环是相对于产品层级的,而它是Team层级的。实践环也是有核心实践和拉动实践组成,大家应该都能猜到这一环的核心实践就是稳定节奏。只有团队保持稳定的节奏,才能使团队风险降到最低,脉冲和毛刺(指交付速度大幅波动)都会带来极大不确定性。其它如持续集成、代码规范都是为了支撑稳定的节奏而服务的。 好了,下面让我们把目光集中到最内层的实践环,也就是洋葱的「芯」,它也由四种实践组成:简单设计、结对编程、TDD、重构。大家猜一下,哪个是核心实践?好,同学应该都能答对,这一环的核心实践就是「简单设计」。它指对于要解决问题域的本质复杂度映射,比如我们要做一款软件来解决某个问题,本身问题是有复杂度的,现假设问题复杂度为100。如果软件要解决这个问题,很自然软件的复杂度就要大于等于100才能hold得住这个问题。而这个软件又是人写的,人要理解和维护这个软件。所以人脑的思维复杂度又要大于等于软件的复杂度。这样,如果我们一个设计能够无限趋近于问题本质复杂度。使我们维护、理解软件的成本最小,这种设计就是简单设计,它是我们追求的目标。我们无论是做重构,做重构我们可以使用结对编程和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之前,首先拿到需求,之后对需求进行分析,分析过程就是把需求按照故事来拆分,然后故事按照场景进行分析,然后每个场景实例化。比如我有一个接口,接口有返回值,返回值具体实例化成0和1。这样需求就会被拆解开来。需求拆解开来之后,针对需求的每个故事,故事的每个场景来写用例。这个时候有两种方法,第一种,先设计出接口然后写用例。另一种先写用例然后再定义接口。大家看一下,觉得哪种方法更合适一点 ?答案这样的,我们推崇针对拆分好的故事和场景先写用例,先用这个用例决定接口什么样子,然后再把接口定义出来。用例针对接口而写,而接口在某个场景下就是代表需求的,从而我的用例就是可以代表需求的 。 用例编写通过编写用例再把接口定义出来,这种方式我们做过实际的测试 这是一个统计数据。如果分支覆盖率达到一定目标,和白盒测试相比,测试代码和生产代码会下降三到四倍。所以说针对需求开发用例性价比会提升。 TDD的做法就是六个字,我们常说的六字真言:红色、绿色、蓝色。红色的意思就是说我把这个需求分析完之后,先把需求进行实现。主要是为了验证接口设计对不对,并没有做具体实现,这个时候编译通过后一跑就是红色。第二个快速实现,就是把很多复杂的策略写死,比如直接return 0或者return 1,绕过去复杂的策略。这个时候,它一跑结果就是绿色。那有同学说了实现这种绿色有什么意义呢?其实一次贯穿的用例至少代表一种业务场景,可以去验证编译和语义环境。第三个蓝色,就是说把用例快速实现然后果断重构,重构过程看看是不是命名充分表达用户语义,横向排版和纵向排版是否整齐,去掉多余的空格和换行使得语义更紧凑,检查逻辑是否可以更顺畅,把所有的重复都消掉,使我们的代码变得海水天空一样湛蓝,这个我们叫做蓝色。然后,变蓝后我再去实现下一个场景,然后再去重复这个过程,红色、绿色、蓝色。那有的同学可能会问,为什么我不能一步把它变成绿色或者一步变成蓝色?为什么还要有一个变红的过程那?这个地方,我们先埋下一个伏笔,后面我们会重点讲述这个问题。 TDD特点接下来我们谈一下TDD的特点,那第一个特点就是驱动开发。 第二个,从客户角度来看接口,还容易看出接口的功能是不是单一,接口是不是易错。比如说,我们经常讲的c语言stringcopy函数,一定是destination放在前面source放在后面,如果设计的接口把source放在前面而destination放在后面,对于一个熟悉c的接口人员就非常可能调用错误。第三个,看看接口是不是有第三方依赖。我们碰到很多的接口用起来非常不爽,因为里面有第三方依赖,还要自己去管理。没有对必要的第三方依赖进行抽取,这个也有利于审视你的接口。所以TDD对改善设计是非常有利的,做TDD确实有一些思考,比如说设计上更本质更背后的一些非常有意义的东西。 第三个特点就是可以快速反馈。在TDD中,所有的用例都可以积累起来而且还是自动化的,所以有问题可以及时发现快速反馈,可以快速形成一个保护网。TDD我们刚才讲了驱动开发,不仅实现需求而且仅仅实现需求,可以消除过度开发和过度设计。快速反馈的另一个作用可以激发你进一步做一件事,比如说在游戏里面打怪或者做某一件事可以马上获得经验值或者宝物,这个可以刺激人的多巴胺神经,让你继续去打怪或者做某一件事情。其他还有很多这种特点,比如说提升用例的表达力啊,这里就不一一累述了。 然后第四点,TDD可以起到一个需求文档的作用,对场景描述表达力非常强。经常使用三方工具看说明文档不一定能懂,但是看用例,把用例抄一遍一般都可以直接运行的。所以说,TDD非常强大抽取非常好的时候,确实可以起到一个文档的作用。 下面我们分享一下TDD里面的关键点,就是TDD有一些步骤是很关键的,这一步我们是不能跳开的。 小步快跑TDD也有这种作用,我们叫做Small win。我们用例红色,然后快速实现绿了,然后快速重构,又绿了。心里多么刺激,然后不停的小步快跑,然后不停的反馈通过这种刺激去做TDD。 测试即文档独立性做好这个独立性,做好隔离就很容易就能定位代码的问题。 以上就是分享就是我们做好TDD的关键点,下面我们看一下做好TDD都有哪些挑战。 第一个叫做Small Step,小步快跑,因为TDD本身要做好前面讲要有一个Small win。通过小步修改,小步提交、小步遍历,从而获得小步胜利的感觉,会获得一种节奏感。大家是否跑过马拉松,我本身在南京包括去年都有参加,跑马拉松最重要的就是节奏感,一旦有节奏感就不容易累。TDD的好处就是获得这种小步快跑的节奏感。 TDD第二个关键点就是要做到依赖隔离,因为我们知道很多的代码跑起来要依赖外部的环境、配置文件、甚至服务,才能跑起来。做TDD时候我们希望快速反馈,所以外部的依赖我们要拨开。这也是TDD的难点之一,我们一般可以通过Mock/stub方法来进行一些依赖的隔离。由对这个hardcode硬代码依赖变成抽象接口数据类型这种方式的依赖,由依赖细节到依赖稳定抽象。然后做测试,通过依赖追踪注入到到生成代码中,这样我的用例就可以跑起来了。 第四个就是独立性,用例独立性。一个用例跑不过之后我可以很清晰的知道是用例的问题还是第三方依赖问题。这也是刚才上面为什么讲要做依赖隔离。很多时候系统上线出问题后,我们很难界定是第三方问题还是自己代码问题 ,有可能就是第三方问题,但是错误实在代码中,所以说很难界定到底责任实在哪。如果我的代码都跑过了,上线后有问题那,那80%可以确定是第三方依赖问题。 最后一个要点叫做非侵入,就是我设计用例或者设计接口的时候,不能为了测试而增加分支或者编译条件。这个时候会对生产代码造成一定的伤害,我们也吃过很多这方面的亏。所以一定通过依赖隔离的方式,把测试分离隔离开,而坚决不采用这种伤害的方式。 下面留三个小问题,同学们自己思考一下! 下面是课堂答疑题: 问题一采用BDD的测试框架,还是xUnit的框架,更适合做TDD? 回答一在coding层面,用xUnit可以快速获得反馈,启动small step和small win的作用;在特性和需求方面,用bdd或atdd,获得就需求的反馈,结合起来 问题二您好,请问对与旧系统的维护使用TDD有哪些建议呢? 回答二新增功能和bug修复可以采用tdd的方式,对3方遗留或旧系统遗留可以采用依赖隔离的方式。让开发人员感受到节奏感,然后逐步扩大tdd的范围 问题三如何在团队内推动tdd,并使其长期有效,有没有好的经验分享? 回答三管理上,采用吸引的方式,邀请tdd熟手经常插花和生手结对,帮传带;技术上,开发TDD框架,进行依赖隔离,让TDD很容易开发。 问题四你工作过程中有多少是采用TDD模式开发?实现推动时需要哪些层面的支持? 回答四自己带的团队都是采用tdd开发,我当时每周抽2天每天2个小时找不同人结对,以点带面,形成氛围。 中生代技术分享文章,在公众号里回复阿拉伯数字1,2,3,4,5,6可以获取如下原创文章
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |