banana 发表于 2021-4-2 12:27:12

如何进行有效的TDD实践

本帖最后由 蓉ZXM 于 2021-4-2 12:31 编辑

一、TDD已死?


最近几年“TDD 已死”的声音不断出现,特别是 David Heinemeier Hansson 那篇文章——《TDD is dead. Long live testing. (DHH)》 ( h... g-live-testing.html )引发了大量的讨论。其中最引人注目的是 Kent Beck、Martin Fowler、David 三人就这个举行的系列对话(辩论)——Is TDD Dead? ( articles/is-tdd-dead/ )
当前国内对 TDD 的理解十分模糊,大部分人也没有明确和有意识的去实施 TDD,因此许多人对此都有着不同的理解。其中最经典的理解就是基于代码的某个单元,使用 Mock 等技术编写单元测试,然后用这个单元测试来驱动开发,抑或是帮助在重构、修改以后进行回归测试。而现在大部分反对 TDD 的声音就是基于这个理解,比如:
[*]工期紧,时间短,写 TDD 太浪费时间;
[*]业务需求变化太快,修改功能都来不及,根本没有时间来写 TDD;
[*]写 TDD 对开发人员的素质要求非常高,普通的开发人员不会写;
[*]TDD 推行的最大问题在于大多数程序员还不会「写测试用例」和「重构」;
[*]由于大量使用 Mock 和 Stub 技术,导致 UT 没有办法测试集成后的功能,对于测试业务价值作用不大;
[*]......
总结一下,技术人员拒绝 TDD 的主要原因在于难度大、工作量大、Mock 的大量使用导致很难测试业务价值等。这些理解主要是建立在片面的理解和实践之上,而在我的认知中,TDD 的核心是:先写测试,并使用它帮助开发人员t来驱动软件开发。
[*]首先是先写测试,这里的测试并不只是单元测试,也不是说一定要使用 mock 和 stub 来做测试。这里的测试就是指软件测试本身,可以是基于代码单元的单元测试,可以是基于业务需求的功能测试,也可以是基于特定验收条件的验收测试。
[*]其次是帮助开发人员,主要是帮助开发人员理解软件的功能需求和验收条件,帮助其思考和设计代码,从而达到驱动开发的目的。
所以 TDD 是包含两部分:ATDD 与 UTDD。




[*]ATDD(Acceptance Test Driven Development):验收驱动测试开发,首先 BA 或者 QA 编写验收测试用例,然后 Dev 通过验收测试来理解需求和验收条件,并编写实现代码直到验收测试用例通过。
由于验收方法和类型也是多种多样的,所以根据验收方法和类型的不同,ATDD 其实是包含 BDD(Behavior Driven Development)、EDD(Example Driven Development),FDD(Feature Driven Development)、CDCD(Consumer Driven Contract Development)等各种的实践方法。比如以软件的行为为验收标准,这个是 BDD;如果以特定的实例数据为验收标准,这个是 EDD;如果以 Web Service API 消费者提出 API 契约来驱动 API 提供者开发 API,这个是 CDCD 等。所以 ATDD 的具体实现需要结合项目的实际情况来选用适合的验收测试方法与类型。
[*]UTDD(Unit Test Driven Development):单元驱动测试开发,首先 Dev 编写单元测试用例,然后编写实现代码直到单元测试通过。这个就是现在很多人所谓的 TDD、实践的 TDD、喜欢的 TDD、抱怨的 TDD,但是它却只是真正意义上 TDD 的一部分而已。



                                                                            TDD 金字塔

               
二、TDD的实施和分层




现在还有非常多软件工程师在质疑 TDD 的可行性,比如太难不会、成本太高无法推动、意义不是很大等,但是他们却一直都在做着 TDD,只不过没有意识到而已,这便是“不识庐山真面目,只缘身在此山中”。
TDD 的实施一般分为思维层面和技术层面。一般来说,思维层面上的实施成本较低、容易接受,但是缺点很多,比如难以传递、难以持续获得快速反馈等;而技术层面上的实施一般成本较高、不容易被人接受,但是优点更多,比如可以获得快速反馈、更容易传递和协作等。而现实世界中 TDD 的实施一般分为三个阶段,即无意识的 TDD、被动通过技术实现的 TDD、以及有意识和主动通过技术实现的 TDD。
第一阶段:无意识的 TDD
对于软件开发人员,当他们拿到一个新的软件需求时,首先会思考如何实现,其中包括当前软件架构、业务分解、实现设计、代码分层、代码实现等。然后通过思考和设计所得到的产出物来驱动代码实现,进而在代码实现中会思考如何通过一个或多个函数或者算法来实现业务逻辑。所以软件系统的实现要先通过意识层面的思考,再进行技术层面的工作。



当开发人员思考和设计这些函数或者方法的时候,一般都会思考它们有哪些参数,然后想象将这些参数换成真实的数据后传递进去,会得到怎样的返回值。好一点的开发人员会思考如何处理异常输入和异常返回值。这类思考其实已经是意识思维上的 TDD,它帮助开发人员先在大脑里面设计并验证代码实现,甚至帮助其重构代码。所以很多开发人员都在无意识的情况下做着 TDD。比如在一个银行系统里面,开发人员拿到一个需求,需要开发一个通过手机 APP 转账的功能。

[*]首先开发人员会基于当前的软件架构思考:是开发一个全新的模块来处理这个业务?还是基于当前架构中的某个模块来添加代码进行处理?
[*]当确定架构和设计之后,就开始思考具体的代码实现,比如类的设计、方法的设计或者函数的设计等。当开发“将钱从原帐号转出”这个功能前,开发人员会思考:这个功能需要支持当钱从原帐号中转出成功后,原帐号中的余额等于原始余额减去转出金额。进一步有些程序员还会设计一些用来验证功能的实例,比如帐号中的原始余额是 999.99,转出 111.11,那么剩余的金额就应该是 888.88。
[*]在这样思考之后,开发人员便开始根据自己大脑中的测试逻辑和用例来驱动和辅助开发过程。在代码开发完毕之后还会想一些办法来验证一下所实现的功能是否符合预期,比如人工使用之前的或者新的测试用例再测试一下。如果验证正确,就会认为自己开发的功能正确了,并交给测试人员进行测试。
其实开发人员在开发前思考测试逻辑和用例的过程就是在做 TDD 了。很多做业务分析的 BA 和测试分析前移的 QA 也同样在无意识的做着 TDD,比如分析验收条件、写出验收文档等。只不过这些 AC 和验收文档可能写得不是很明确或者不是很好,比如不是实例化需求等,但是本质上已经是 TDD 了。只不过是初级的无意识的 TDD,可能有的人做得好,有的人做得不好,而且没有明确的产出来协助和规范这个测试驱动开发方式,也缺乏快速反馈、度量、传递和协作等。因此从无意识到有意识将是做好 TDD 的一个重要过渡。
第二阶段:被动通过技术实现 TDD
当有一部分软件工程师意识到了 TDD 的意义和普遍存在性之后,就开始准备解决思维上的 TDD 的缺点。而解决这些问题的方法就是在技术层面上用代码来实现 TDD,用明确的代码来协助和规范开发人员的测试驱动开发行为,来度量他对业务逻辑以及代码实现的理解度。通过将他的理解传递给以后的维护人员,让他的理解能重复被使用,以及和其他人协作开发。但是现实中很多开发人员的认识不足以及技术能力不够,就算管理层支持并且主动推动 TDD,最终由于开发人员设计和选取的测试用例合理性很差,导致驱动出来的代码有效性差,测试用例无法体现出 SBE(Specification by example)导致易读性差,对于自动化测试框架和测试编写不熟悉导致开发速度很慢等,往往是被动的在技术层面上去实现 TDD,所以出现了各种怨言,各种抵触,进而导致技术层面上的 TDD 很难以大规模实施。由于意识层面上的难易程度和工作量都比技术层面上相对较小,所以前者实施起来相对容易一些,而后者则相对较难,所以如果通过了各种手段强行实施 TDD,而没有主动去摆正做 TDD 的意识,甚至没有足够的技术能力,那么这样的 TDD 就是一个倒三角,非常容易倒塌。


TDD 倒三角
所以,如果不希望技术层面上的 TDD 随时倒塌,就需要把这个倒三角补全,才能更好的、长久的实施 TDD。
第三阶段:有意识和主动通过技术实现 TDD
为了大规模以及有效的实施 TDD,首先要突破思维意识的局限,认识到 TDD 的普遍存在性和适用性,不要害怕和排斥 TDD 这种思维和开发模式。其次要主动学习,并刻意练习 TDD 的技术实现,提升自己的技术能力,从而在技术层面能更容易的实现 TDD,摆脱被动 TDD 的困境。其中学习的方法包括阅读 TDD 相关的书籍和文章,书籍包括《测试驱动开发》、《重构》、《BDD In Action》以及《系统思考》等,从而充分理解 TDD 优点和局限。对于刻意练习,一定要长时间坚持去做,让其成为一种习惯。如果在项目中没有合适的环境去练习,还可以通过一些第三方的 TDD 练习系统去做刻意练习,比如 Cyber-dojo()。只有大量的刻意练习才能让你在真实的代码编写过程中去思考和理解 TDD,去运用你通过学习得到的知识,最终才能做到有意识和主动的通过技术去实现 TDD,TDD 的倒三角才能变成一个稳定的砖块,然后哪里需要往哪里搬。



                                                                                    TDD 砖块

三、TDD实践

最近TDD相关的培训和讨论也越来越多,比如:

[*]学好TDD靠多多练习就可以了,不用学习其理论知识;
[*]开发应该自己理解业务,并提炼测试(需求)点来实施TDD;
[*]开发只要做好TDD,就不需要其他人测试了;
[*]软件没有做好就是因为TDD没有做好;
[*]等等。
而我对于这些片面的说法都是不赞同的。
[*]首先,TDD不是银弹,所以软件没有做好的原因是很多的,也不是靠TDD做好了就一定能做好。
[*]其次,学习TDD的理论是非常重要的,而仅仅靠不断练习并自悟的方法,对于大部分普通人来说是难以成功的。
[*]最后,一般开发能做的较好的TDD,一般是UTDD,而对于ATDD,则需要相应的业务分析,测试分析与设计的相关方法和技术。如果要求开发做好ATDD,则需要开发学习并掌握相应的业务分析,测试分析与设计的相关方法和技术。而这对于开发也是非常大的挑战。所以在真正的TDD实践中,如果想大规模实施TDD,仍然需要相应的分工才更可行,更容易实施。而提前充分学习并了解TDD实践的相关理论,也是可以帮助实施人员更好的去实践,少踩坑,从而正其思规其行。

3.1 TDD需要的能力


在真实工作中要较好的实施TDD需要具备以下能力:
[*]测试前移(左移)的思维能力
[*]软件设计能力
[*]业务和技术需求分析和任务拆分能力
[*]测试用例分析和设计能力
[*]自动化测试开发能力
[*]代码重构和持续改进能力
首先是测试前移(左移)的思维能力是TDD实施的前提条件。如果没有这个思维能力,或者不认可这个测试前移(左移)的价值,这样的开发人员则很难认可TDD的开发方式,从而可能会想尽各种办法抵制TDD,或者阳奉阴违,从而导致TDD实施艰难,并显得困难重重。所以一定要拥有这个能力作为前提才容易真正实施好TDD。要实施TDD,第一步就是业务需要分析能力,其次根据需求点设计测试用例的能力。这里面包含了一个业务需求人员和测试人员的基本能力。其中业务需求分析能力需要能很好的分析业务需求,并能总结出业务需求点或者业务验收点。其次测试用例设计能力需要能根据业务的需求点或者业务验收点设计出有效的正确的测试用例,从而才能驱动出功能正确的业务代码。实施TDD最为核心的两个能力则是自动化测试开发能力和代码重构能力。其中自动化测试开发能力是指熟练使用相应的自动化测试框架和编程语言,将前面设计出来的测试用例自动化起来。对于UTDD常用的自动化测试框架有JUnit,Jasmine等,而对于ATDD常用的自动化测试框架则有Cucumber,RobotFramework等。只有将测试用例自动化之后,才能快速的进行回归测试,从而帮助代码重构。而良好的代码重构能力,则是代码质量内建,防止代码腐化以及保障代码易于维护的主要手段之一。如果没有能力或者不愿对代码进行重构,那么就不能算一个完整的TDD。最后要实施一套完整的好的TDD,还需要一个持续改进的能力。不仅需要对代码进行持续改进,即代码重构;还需要对自动化测试的代码,测试用例设计和业务分析进行持续改进。只有这样对TDD的各个步骤和环节都进行持续改进,才能越来越好的实施TDD。

3.2 UTDD和ATDD的步骤



在实际工作中,实践TDD第一步就是转变思维-测试前移(及测试左移),将测试用例分析,设计和实现前移到编写代码之前。这里的测试并不只是单元测试,也不是说一定要使用mock和stub来做测试。这里的测试就是指软件测试本身,可以是基于代码单元的单元测试,也可以是基于业务需求的功能测试,也可以是基于特定验收条件的验收测试。其次是帮助开发人员,主要是帮助开发人员理解软件的功能需求和验收条件,帮助其思考和设计代码,从而达到驱动开发的目的。所以TDD可以被分为UTDD(Unit TDD)和ATDD(Acceptance TDD),其中UTDD是指代码单元级别的驱动开发,而ATDD是指功能验收级别的驱动开发。所以TDD可以帮助开发人员梳理和理解需求 ,帮助开发人员获得更好的代码设计 ,并且有效的减少过度设计,获得大量有效的测试用例(手动/自动), 以及可以获得快速反馈,从而有效的减少返工,提高代码的内在质量。对于UTDD(单元驱动测试开发),首先由开发人员自己或者开发人员结对业务或者测试人员一起分析并梳理需求点,然后开发人员针对每个需求点编写自动化单元测试用例,并实现代码直到单元测试通过。UTDD中的测试主要针对函数(方法)或者业务单元代码的测试,而且一定要自动化,这样才能在开发的过程中快速的反复的执行它们,从而达到驱动开发的目的。最后也可以在持续集成流水线快速反复的执行他们,从而帮助持续集成获得单元测试层面上的快速反馈。UTDD的特点如下:
[*]关注单元级别的代码设计
[*]测试用例需要明确的实例
[*]清晰的单元完成标志
[*]最快的feedback周期
[*]有效的减少开发过程中side effect引起的返工
[*]可以帮助开发减少调式的成本
[*]可以作为单元接口的使用文档
而对于ATDD(验收驱动测试开发),首先BA或者QA编写验收测试用例,然后Dev通过验收测试来理解需求和验收条件,并编写实现代码直到验收测试用例通过。由于验收方法和类型也是多种多样的,所以根据验收方法和类型的不同ATDD又可以分为BDD(Behavior Driven Development),EDD(Example Driven Development),FDD(Feature Driven Development),CDCD(Consumer Driven Contact Development)等具体的实践方法。比如以用户使用软件时软件的行为为验收标准,这个是BDD;如果以特定的实例数据为验收标准,这个是EDD;如果以Web Service API消费者提出API契约来驱动API提供者开发API,这个是CDCD等。所以ATDD的具体实现需要结合项目的实际情况,选用适合的验收测试方法与类型。ATDD的特点如下:
[*]关注业务价值,测试与需求一体化
[*]明确的测试实例(SBE)而不是复杂的描述
[*]清晰的功能完成标志
[*]更快的feedback周期,提早并频繁沟通
[*]消除误解,减少返工
[*]可视化的验收回归测试
[*]可以作为描述功能的活文档


测试驱动开发的实施有一个经典三步曲,不论是UTDD还是ATDD都可以按照这个三步来实施:
[*]变红:写一个不通过的测试 (红)
[*]变绿:写实现代码,使其刚好通过测试 (绿)
[*]重构
但是要实施好TDD,不能只靠这三个核心步骤,还需要相应的其他辅助步骤以及多方协作,下面两个图分别展示了TDD的步骤和协作的基本全貌。
TDD实施-步骤


TDD实施-步骤












四、最后
业界元老Robert C. Martin 也提出过他总结的TDD三原则:

[*]不允许编写任何产品代码,除非目的是为了让失败的测试通过;
[*]不允许编写多于一个的失败测试,编译错误也是失败;
[*]不允许编写多于恰好能让测试通过的产品代码,有效的减少返工。
这三个原则很好的总结了TDD实践的关键步骤。此三原则虽然是正确的,但是严格按照此三个原则去做却是不易的,并且在现实的开发工作中并不是很多人能严格按照此三原则去编写代码,因为转变思维是一件很困难的事情,而且如果在时间短交付压力大的情况下就更为困难了。但是在我经历的项目中,我们一般以ATDD结合UTDD的模式进行工作,并根据资源的多少决定其比例,从而全方位保证代码的内在质量和业务正确性。(IDCF)

页: [1]
查看完整版本: 如何进行有效的TDD实践