今天能写下这篇《单元测试驱动开发之旅》,源于近期的工作——会员客户管理中心项目的开发。会员客户管理中心,作为未来运营管理体系中的一个基础支撑系统;解决现有问题,规范会员客户基础数据的使用,提供权威的会员客户数据。
会员客户管理中心,立项之初就已确认“以DDD(领域驱动设计)的思想指导项目开发”这一方针。起初,虽团队成员对DDD知之甚少,但是以DDD思想指导项目开发已是板上钉钉的事,我们所能做的就是啃下DDD这块硬骨头。在翻阅大量的DDD相关资料和书籍时,发现有一词与之形影不离——TDD,就这样一颗有关TDD的种子也在悄无声息地埋下了。
为了实现 “标准动作做标准——DDD更标准落地”这一目标,所以我们在项目实现阶段,搭建了DDD框架、采用了JPA技术,引入了TDD思想。伴随着TDD思想的引入,会客团队的单元测试驱动开发之旅也就这样开启了。
通常提及“TDD"一词时,"UTDD"、"ATDD"、"BDD"等名词也会被提及;正如你所见,它们看起来非常相似——“xDD”。
正是因为这“外表"的相似性,它们在我们眼中就如孪生兄弟一般,总是让人傻傻分不清。再加上,网上文章对它们的定义又是千差万别,很容易把人弄得一头雾水。
所以,在开启单元测试驱动开发之旅前,我们需要做一件事:“理清它们之间的关系”。
2.1.关系图谱
TDD有狭义、广义之分。狭义上,TDD 特指UTDD;广义上,TDD 包括UTDD、ATDD。
- 在代码层面,进行TDD(测试驱动开发),称为UTDD(单元测试驱动开发);
- 在业务层面,进行TDD(测试驱动开发),则称为ATDD(验收测试驱动开发)。
- ATDD由于验收方法和类型的多样性,可细分为BDD、EDD、FDD、CDCD等。
为了消除歧义,此处TDD我们统一采用广义上的说法。关系图谱如下所示:
我们可以把 TDD、UTDD、ATDD、BDD几者的关系,理解为三世同堂。
- TDD(测试驱动开发)看作“爷爷”,是掌舵者,一切思想的起源(软件质量内建)。
- ATDD(验收测试驱动开发)看作“父亲”,负责在外打拼挣钱养家——“男主外”(在业务层面,确保软件的功能特性是符合业务预期的)。
- UTDD(单元测试驱动开发)看作“母亲”,负责家中各种大小事务——“女主内”(在代码层面,确保软件的实现代码是整洁可用的)。
- BDD(行为驱动开发)看作“孩子”,是新一代青年,继承了父亲ATDD的衣钵,并做到了 “青出于蓝,而胜于蓝”(验收标准实例化)。
UTDD,英文全称:Unit Test Driven Development ,中文全称:单元测试驱动开发;是一种敏捷软件开发的技术,它期望通过单元测试用例来驱动软件代码的实现。
TDD的实施有一个经典三步曲,不论是UTDD还是ATDD都可以按照这三步来实施:“变红->变绿->重构”。对于UTDD而言,这经典三步曲的具体含义如下:
1.变红:编写一个刚好运行失败的单元测试用例。
2.变绿:填充一段刚好通过用例的程序逻辑代码。
3.重构:更整洁的单元测试用例、程序逻辑代码。
但是要实施好UTDD,不能只靠这三个核心步骤,还需要相应的其他辅助步骤以及多方协作,下图分别展示了ATDD、UTDD的步骤和协作的基本全貌。
通过前面的介绍,我们已对UTDD的家族图谱以及基础知识有了一定认识;所以本小节聚焦于UTDD在会员客户管理中心的落地实践。
“工欲善其事,必先利其器。”在实践UTDD之前,我们需要能让UTDD更好落地的工具。会员客户管理中心的UTDD工具集 = JUnit + AssertJ +Mockito
- JUnit:一个Java语言的单元测试框架。用它来执行单元测试用例。
- AssertJ:一个Java语言的流式断言器。用它来断言单元测试结果。
- Mockito:一个Java语言的Mocking框架。用它来构造单元测试替身。
上述UTDD工具集的安装教程、使用教程比较简单,可自行查阅网上资料进行实践。
“纸上得来终觉浅,绝知此事要躬行”。结合书籍理论、项目实践,对实践TDD系列敏捷开发技术时,有了以下浅薄认识:
第一步就是转变思维——测试左移,将测试用例分析、设计和实现,移至编写代码之前。
第二步就是了解和学习测试基础理论,做到能解答“什么是单元测试?”“单元测试测哪些内容、哪些地方值得测试?”、“优秀的单元测试应具备哪些品质?”等系列问题,具体可参考 Right-BICEP 、CORRECT、FIRST原则等内容进行学习。
第三步就是结合项目进行具体实践,因为“说一千道一万,不如亲身实践一遍”。
下图源于《有效的单元测试》,是作者结合工作总结提炼而成的。我们可以仿照此图,从 测试的执行速度、测试的可阅读性、测试的可维护性、测试的可靠性等几个方面给出具体建议。
4.3.1.测试的执行速度
首先我们可以尝试执行已有的单元测试,看看具体的执行速度是怎么样的?若执行时间超越秒级计数,那毫无疑问你需要为该单元测试的执行速度提提速。那怎么样能提高测试的执行速度?观察我们所执行的单元测试,不难发现项目在执行单元测试时,需先初始化一些相关依赖项,而正是这些依赖项拖慢了执行速度。所以,我们只需减少或不依赖 相关外部基础设施、组件、spring容器等即可!
若能结合领域驱动设计思想,使用领域驱动设计框架,我们只需对领域层进行单元测试即可。补充说明:领域驱动框架的领域层用于存放 领域模型,往往为了保证领域层的纯粹性,而不会直接依赖相关的外部基础设施、组件、spring容器等。
4.3.2.测试的可阅读性
依据《与代码共同演进的活文档》的观点,我们所编写的单元测试可看做是一份精炼的文档。那么问题就由“怎么让测试具有可阅读性?”变成了“怎么让文档具有可阅读性?”。
个人理解,可阅读性的文档往往是规范化的、结构化的,所以我们可以从规范化、结构化两方面入手提升可阅读性。
没有必要的重复是不好的,它增加了代码的不透明性,使得散落在各处的代码难以理解。此外,当变更重复项时,每修改一处重复都是而外开销,若遗漏了修改某处会增加出现Bug的机会。
个人理解,单元测试除了分析、设计测试用例外,其余的主要是构造场景数据,紧接着 使用场景数据 验证预期逻辑。而往往就是在构造场景数据时,会出现大量的重复项。
4.3.4.测试的可信赖性
- 禁止测试用例答非所问,方法名的测试意图与方法体的具体实现 保持一致。
- 拒绝永不失败的测试,即不允许测试用例没有断言。(没有断言的测试是无价值的)
- 测试用例不应降低期望,即不允许为了通过测试而降低测试用例的确定性、精确性。(长期看来,这种测试由于不够精确,会造成一种虚假的安全感)心得体会
4.4.1.明显好处
降低开发时的思绪负担
在以往软件开发工作中,常会有“思绪万千” 的感觉(不知从哪动手、如何动手);而当采用UTDD后能有效解决这一问题,只需照UTDD的基本流程:变红->变绿->重构,按部就班进行即可。其实这和软件分层架构的思想 如出一辙——“关注点分离”。我们在软件编程的过程中,主要几个关注点:需求、设计、实现。
变红:写一个让程序运行失败的单元测试用例,它是对一个小需求的描述,只需要关心输入输出,这个时候根本不用关心如何实现。(需求)
变绿:专注实现当前需求,不关心其他需求,也不管代码质量是多么惨不忍睹。(实现)
重构:既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,借鉴《重构——改善既有代码》中的手法一并消除它们,让代码变成整洁的代码。(设计)
督促写出整洁可用的代码
Kent Beck在《测试驱动开发》一书中,就开宗明义地提出:TDD的所追求的目标是 Clean code that works(代码简洁可用)。Code that works: 代码首先必须可用——单元测试。Clean code: 代码应该尽可能简洁——重构。
在编写单元测试用例之前,你需要进行 需求分析、考虑程序的使用性、可测试性。并且你只能填充刚好让用例通过的程序逻辑代码,从而有效避免了过度设计,软件代码做到了 “可用”。正因你频繁地重构单元测试用例、程序逻辑代码,软件代码做到了 “整洁”。
协助达成质量内建的目标
采用UTDD的第一步是转变思维——测试左移,将测试用例分析、设计和实现,前移至编写代码之前。这是一种极大程度的测试左移,那是不是能极大程度享受测试左移带来的好处?理想情况,你所写的每一行程序逻辑代码,都是为了通过单元测试用例;换而言之,你所写的每一行程序逻辑代码,都通过了单元测试用例。
默默编制一张系统保护网
采用UTDD进行程序实现的同时,无形中也在为软件系统编织一张保护网(单元测试用例)。当有新的变化需求或重构代码时,你可以大刀阔斧地去做,因为有张保护网替你兜底(回归测试)。即使时过境迁,这张保护网依然能有效地保护系统的边界,能避免让你的系统成为“烫手的山芋”。
4.4.2.潜在风险
凡事都有两面性,UTDD的引入也是有代价和风险的。
- 学习成本:团队人员初学UTDD,团队推广实践UTDD都是有成本的。
- 开发成本:开发人员需要编写、维护测试用例,所带来的时间、工作量成本。
- 潜在问题:Mock的大量使用导致很难测试业务价值。若没能在宏观上把控“做正确的事”一原则(ATDD),只是追求“把事情做正确”会掉入“虚假的安全感”陷阱(UTDD)。