一篇文章教你在业务开发中高效玩转 TDD(测试驱动开发)
一,TDD(Test-Driven Development)介绍:
1,TDD:敏捷开发中的一项核心实践和技术,也是一种设计方法论。从根本上来讲,TDD 的定义还是比较抽象的
TDD 的原理是在 开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。
2,步骤:
先写测试代码,并执行,但需要断言得到失败结果——红:
写实现代码让测试通过——绿
重构代码,并保证测试通过——重构(其实按照单项目来说,可简单认为是代码的变更)
反复实行这个步骤 测试失败 -> 测试成功 -> 重构。
3,优点:
在任意一个开发节点都可以拿出一个可以使用,含少量 bug 并具一定功能和能够发布的产品。
保障代码的正确性,能够迅速发现、定位 bug。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位 bug 提供了条件。
缺点:
增加代码量。测试代码是系统代码的两倍或更多,但是同时节省了调试程序及挑错时间。
在实际上的业务开发上来说,如果要完全实现 TDD 的整体操作,将会大大增加开发人员的工作量
4,原则(讲的比较宽泛):
测试隔离:不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节。
及时重构:无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。
可测试性:功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。
先写断言:测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。
测试驱动:这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。
测试列表:需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。
小步前进:把所有的规模大、复杂性高的工作,分解成小的任务来完成,每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性、
一顶帽子:开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。
5,从主流言论而言,推行 TDD 的障碍大约有如下几点:
开发人员的质量意识:大部分开发人员,编写出的代码考虑的点总是不全的,或是懒,或是缺乏编写测试用例的意识;
分析需求并进行任务分解的能力; 需求分析能力常常是开发人员的短板。开发人员养成了一个习惯,看什么事情都会从技术实现的角度去思考。要实现一个网页,就会想到如何编写 JavaScript 来响应用户的动作,如何编写 CSS,却不会去思考用户体验和操作的流程。
将测试作为开发起点的开发习惯: 开发人员,一般都是先写代码,再去测试;
开发人员的重构能力,包括如何识别“坏味道”和如何运用重构手法: 在代码版本变更的时候,能通过合适的“重构”,去保证改动小的情况下,能满足新的需求,对于“坏味道”的定义:baijiahao.baidu.com/s?id=171346…
单元测试的基础设施,尤其是测试数据准备: 以及执行自动化单元测试的工具,类似于 gitlab 的 runner 自动执行和对应的框架
6,对于 37 手游平台本身来说,对于单元测试的基础设施,以及 bad smell 的识别是已经具备的(通过 sonarqube 和 gitlab runner)所以 TDD 在手游平台的最大实现难度,个人认为主要在于开发人员自身的意识。
二,TDD 实际使用过程和分析:
1,举个例子,当我们想要编写一个钱包功能,该钱包能存钱,以及能看到自己有多少钱
(1)首先,我们先写测试用例
(2)上面的那一段只是在写一段伪代码,实际上,编译器压根就不会编译过,这里就到了 TDD 的红阶段:
用最短的代码,来编写编译器能通过,但断言失败的代码
(3)此时由于断言失败,这时候应该用足够的代码,去编写让测试用例通过,这就是 TDD 的绿阶段:
这时候我们会发现,由于开发人员的“失误”,提现功能和存储功能会耦合到一个函数里了,这时候,就要到了 TDD 中的重构阶段,
单论这个功能而言,要做的其实就是拆分提现为单独的函数,此时也是先编写最少的代码使编译通过,但测试断言不通过
再通过实现 withdraw 这一函数,使测试通过
至此,钱包的基本功能已经完成,这时候如果有产品提出更多的需求,也是按这个流程进行开发
实际上的 TDD 的使用,就是这样反复的红——绿——重构过程
2,go 的示例项目地址:studygolang.gitbook.io/learn-go-wi…
3,从业务开发的实际使用的方法论:从实际上的使用来说,TDD 实际上花费时间最多的在于红-绿循环,即测试失败到测试成功的过程;但大部分人在没有 TDD 的意识时,总会在“红”这一阶段寸步难行,因此在这一步骤前,可以采用这样的方式来实现
1,大致构思软件被使用的方式,把握对外接口的方向——————这一步一般在方案设计里会有体现
2,大致构思功能的实现方式,划分所需的组件(Component)以及组件间的关系(所谓的架构)。当然,如果没思路,也可以不划分——————按平台的方案设计来说也会有体现
3,根据需求的功能描述拆分功能点,功能点要考虑正确路径(Happy Path)和边界条件(Sad Path)————像测试一样,在开发需求前有自己的测试用例,并且拆分任务列表
4,依照组件以及组件间的关系,将功能拆分到对应组件——————如 dao 层的对 mysql 一些 crud 函数设计用例,对 redis 的 crud 设计用例
5,针对拆分的结果编写测试,进入红 / 绿 / 重构循环——————实际应用 TDD 方式进行开发
三,在手游平台业务开发中的使用
1,在实际的业务开发中,我们其实如果完全按照 TDD 的思想去实现,对开发人员来说的负担很大,而且很可能到最后会成为开发人员的枷锁,TDD 红绿循环的本质,个人认为其实是一个很浅显易懂的道理:其实就是在写功能的同时,自然而然的就把测试用例给完善了,你懂的如何设计测试用例,如何写 bug,写出来的功能自然也是完善的。
2,因此个人提倡,在业务开发中的 TDD,可简化为以下步骤:
拿到需求后,根据需求文档,先进行方案设计,划分功能模块———— 具体可参考技术方案模版,在这一步骤就可以把模块,架构,对外接口进行设计
根据方案模版,拆分开发的任务列表———— 这里可根据功能模块进行拆分,列出具体的任务。
根据任务列表,按 TDD 的方式编写代码,但循环次数根据情况做自己的考虑——— 实际的开发过程,应该是先编写某个模块的功能代码,再编写数据操作类型的功能代码,最后通过红绿循环将整体串起来;不过从业务上来讲,你的功能拆分的越细,测试用例也会随着越详细,系统的可靠性自然而言也会随之提升。
3,具体例子:举一个我们业务上用到的人脸识别功能为例
方案设计,划分功能模块:根据产品文档,我们可以看出拆分出两个功能大模块——本体人脸认证服务和人脸识别配置后台
根据方案模版,拆分开发的任务列表: 从上面的功能模块,我们能更细化的拆分小功能,如人脸认证服务需要提供三个接口,分别是
此时可列出具体开发的任务列表 TODO LIST:
根据任务列表,按 TDD 的方式编写代码: 这里由于篇幅原因,只根据 发起人脸识别功能为例
先用最短的代码,让编译器通过,但断言失败的代码:因为 req 和 InitFaceVerifyDetail,只进行了声明,对应的结果肯定是为空的,所以测试用例会不通过
再用足够的代码,让测试用例能够通过:这里我们把编写可以拉宽一点,最终呈现的样式为这样:
这时候我们会发现,总体调用阿里云那边的接口是通了的,但实际因为插入数据库并没有具体的编写,导致插库本身是失败的;因此针对库本身我们也可以有测试用例,框架自身也是支持的
重复 TDD 的流程即可
另外,除了发起人脸识别验证成功外,我们还应该有发起人脸识别验证失败的情况,因此测试用例也可添加失败时候的情况
然后重复 TDD 的流程即可
四,单元测试用例的编写:
(1)编写规范:这里只做介绍,实际的测试用例编写还是要根据业务开发而定的
AIR 原则具体包括:
A: Automatic (自动化)
I: Independent (独立性)
R: Repeatable (可重复)
BCDE 原则:
B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点,数据顺序。
C: Correct,正确的输入,并得到预期的结果。
D: Design,与设计文档相结合,来编写单元测试。
E: Error,单元测试的目的是证明程序有错,而不是证明程序无错。为了发现代码中潜在的错误,我们需奥在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果
(2)达到的质量标准:
针对框架代码我们可以要求覆盖率低一些,比如 50%即可,能覆盖主逻辑。核心逻辑代码的覆盖率越高越好,最好能达到 100%。
(3)go test 自带的-cover 命令能输出对应的代码覆盖率,现在框架生成自带的 gitlab-ci.yml 文件也有自带这个命令,编写完测试用例后,我们应该查看对应的测试用例是否能覆盖主流程大部分的代码,如果不能,则可认为编写的单元测试用例是不充分的。
同时,sonarqube 上也有对应的测试代码覆盖率可以观看
(4)对于 go test 中的测试用例,除了普通的业务逻辑单元测试用例之外,其实还存在基准测试用例(BenchMark 开头,用于测试函数性能),性能比较函数测试用例等等,由于基准测试这块在实际的业务开发中,如果强求会过于吹毛求疵,因此在这不做过多描述
具体可看 xiaoming.net.cn/2021/03/16/…
五,总结
TDD 固然是一个好东西,但也要切记勿过度设计,不复杂化,在合理的范围内进行 TDD,才是真正能保证我们自身开发的质量
评论