单元测试驱动开发之旅

1.事出必有因

        今天能写下这篇《单元测试驱动开发之旅》,源于近期的工作——会员客户管理中心项目的开发。会员客户管理中心,作为未来运营管理体系中的一个基础支撑系统;解决现有问题,规范会员客户基础数据的使用,提供权威的会员客户数据

        会员客户管理中心,立项之初就已确认“以DDD(领域驱动设计)的思想指导项目开发”这一方针。起初,虽团队成员对DDD知之甚少,但是以DDD思想指导项目开发已是板上钉钉的事,我们所能做的就是啃下DDD这块硬骨头。在翻阅大量的DDD相关资料和书籍时,发现有一词与之形影不离——TDD,就这样一颗有关TDD的种子也在悄无声息地埋下了。

        为了实现 “标准动作做标准——DDD更标准落地”这一目标,所以我们在项目实现阶段,搭建了DDD框架、采用了JPA技术,引入了TDD思想。伴随着TDD思想的引入,会客团队的单元测试驱动开发之旅也就这样开启了。

2.溯本求源

        通常提及“TDD"一词时,"UTDD"、"ATDD"、"BDD"等名词也会被提及;正如你所见,它们看起来非常相似——“xDD”

        正是因为这“外表"的相似性,它们在我们眼中就如孪生兄弟一般,总是让人傻傻分不清。再加上,网上文章对它们的定义又是千差万别,很容易把人弄得一头雾水

        所以,在开启单元测试驱动开发之旅前,我们需要做一件事:“理清它们之间的关系”。

2.1.关系图谱

TDD有狭义、广义之分狭义上,TDD 特指UTDD;广义上,TDD 包括UTDD、ATDD。

  1. 代码层面,进行TDD(测试驱动开发),称为UTDD(单元测试驱动开发);
  2. 业务层面,进行TDD(测试驱动开发),则称为ATDD(验收测试驱动开发)。
  3. ATDD由于验收方法类型的多样性,可细分为BDDEDDFDDCDCD等。

    为了消除歧义,此处TDD我们统一采用广义上的说法。关系图谱如下所示:

2.2.三世同堂

        我们可以把 TDD、UTDD、ATDD、BDD几者的关系,理解为三世同堂

  1. TDD(测试驱动开发看作“爷爷是掌舵者,一切思想的起源(软件质量内建)。
  2. ATDD(验收测试驱动开发看作“父亲负责在外打拼挣钱养家——“男主外”(在业务层面,确保软件的功能特性是符合业务预期的)。
  3. UTDD(单元测试驱动开发看作“母亲负责家中各种大小事务——“女主内”(在代码层面,确保软件的实现代码是整洁可用的)。
  4. BDD(行为驱动开发)看作“孩子”,是新一代青年,继承了父亲ATDD的衣钵,并做到了 “青出于蓝,而胜于蓝”(验收标准实例化)。

3.姓甚名谁

        UTDD英文全称:Unit Test Driven Development ,中文全称:单元测试驱动开发是一种敏捷软件开发的技术,它期望通过单元测试用例来驱动软件代码的实现

3.1.实施流程

        TDD的实施有一个经典三步曲,不论是UTDD还是ATDD都可以按照这三步来实施:变红->变绿->重构对于UTDD而言,这经典三步曲的具体含义如下:

        1.变红:编写一个刚好运行失败的单元测试用例

        2.变绿:填充一段刚好通过用例的程序逻辑代码

        3.重构更整洁单元测试用例程序逻辑代码

        但是要实施好UTDD,不能只靠这三个核心步骤,还需要相应的其他辅助步骤以及多方协作,下图分别展示了ATDD、UTDD的步骤和协作的基本全貌。

4.为我所用

通过前面的介绍,我们已对UTDD的家族图谱以及基础知识有了一定认识;所以本小节聚焦于UTDD在会员客户管理中心的落地实践。

4.1.实践工具

        “工欲善其事,必先利其器。”在实践UTDD之前,我们需要能让UTDD更好落地的工具。会员客户管理中心的UTDD工具集 = JUnit + AssertJ +Mockito

  1. JUnit:一个Java语言的单元测试框架。用它来执行单元测试用例。
  2. AssertJ:一个Java语言的流式断言器。用它来断言单元测试结果。
  3. Mockito:一个Java语言的Mocking框架。用它来构造单元测试替身。

        上述UTDD工具集的安装教程使用教程比较简单,可自行查阅网上资料进行实践。

4.2.实践经验

        “纸上得来终觉浅,绝知此事要躬行结合书籍理论、项目实践,对实践TDD系列敏捷开发技术时,有了以下浅薄认识:

        第一步就是转变思维——测试左移,将测试用例分析设计和实现编写代码之前。

       第二步就是了解和学习测试基础理论,做到能解答“什么是单元测试?”“单元测试测哪些内容、哪些地方值得测试?”、“优秀的单元测试应具备哪些品质?”等系列问题,具体可参考 Right-BICEP 、CORRECTFIRST原则等内容进行学习

        第三步就是结合项目进行具体实践,因为“说一千道一万,不如亲身实践一遍”。

4.3.实用建议

        下图源于《有效的单元测试》,是作者结合工作总结提炼而成的。我们可以仿照此图,从 测试的执行速度测试的可阅读测试的可维护性测试的可靠性等几个方面给出具体建议。

4.3.1.测试的执行速度

        首先我们可以尝试执行已有的单元测试,看看具体的执行速度是怎么样的?若执行时间超越秒级计数,那毫无疑问你需要为该单元测试的执行速度提提速。那怎么样能提高测试的执行速度?观察我们所执行的单元测试,不难发现项目在执行单元测试时,需先初始化一些相关依赖项,而正是这些依赖项拖慢执行速度所以,我们只需减少或不依赖 相关外部基础设施、组件、spring容器等即可!

        若能结合领域驱动设计思想,使用领域驱动设计框架,我们只需对领域层进行单元测试即可。补充说明:领域驱动框架的领域层用于存放 领域模型,往往为了保证领域层的纯粹性,而不会直接依赖相关的外部基础设施、组件、spring容器等。

4.3.2.测试的可阅读性

        依据《与代码共同演进的活文档》的观点,我们所编写的单元测试可看做是一份精炼的文档。那么问题就由“怎么让测试具有可阅读性?”变成了“怎么让文档具有可阅读性?”。

        个人理解可阅读性的文档往往是规范化的结构化的,所以我们可以从规范化、结构化两方面入手提升可阅读性。

        规范化:

  1. 单元测试方法名仿照 Ruby 语言方法进行命名,并且方法名应表达出测试意图示例:firmNumber_cannot_be_null_or_empty。
  2. 单元测试方法体应遵循Given-When-Then模式,这样能清晰描述了测试的准备预期的行为具体验收条件
  3. 更多编码细节规范,可参考《阿里编码规约》、代码整洁之道等书籍。

        结构化:

  1. 单元测试需避免成为“大泥球”,其应类似“乐高积木”由一块块积木搭建而成。
  2. 善用 Junit5 的特性: @Nested@DisplayName、参数化方法。
  3. 更多结构化建议,可参考结构化写作》、《金字塔原理等书籍。

4.3.3.测试的可维护性

        若对程序员提问“什么是万恶之源?”,相信绝大多数人都会回答“重复”。简单说,重复是存在多份拷贝对单一概念的多次表达——这些都是没有必要的重复

        没有必要的重复是不好的,它增加了代码不透明性使得散落在各处的代码难以理解此外,当变更重复项时,每修改一处重复都是而外开销,若遗漏了修改某处会增加出现Bug的机会。

        个人理解,单元测试除了分析、设计测试用例外,其余的主要是构造场景数据,紧接着 使用场景数据 验证预期逻辑。而往往就是在构造场景数据时,会出现大量的重复项。

        针对以上问题,我们需要引入“数据夹具”这一概念。数据夹具实际上是为测试准备一系列状态对象工具类借助数据夹具一是可以有效消除重复代码(同时也便于维护),二是能极大调用开发者的情绪(只需传几个关键参数,然后由数据夹具统一返回预期对象)。

4.3.4.测试的可信赖性

  1. 禁止测试用例答非所问方法名测试意图方法体具体实现 保持一致
  2. 拒绝永不失败的测试,即不允许测试用例没有断言(没有断言的测试是无价值的)
  3. 测试用例不应降低期望,即不允许为了通过测试降低测试用例的确定性精确性。(长期看来,这种测试由于不够精确,会造成一种虚假的安全感)心得体会

4.4.心得体会

4.4.1.明显好处

降低开发时的思绪负担

        在以往软件开发工作中,常会有“思绪万千” 的感觉(不知从哪动手、如何动手);而当采用UTDD后能有效解决这一问题,只需照UTDD的基本流程:变红->绿->重构,按部就班进行即可。其实这和软件分层架构的思想 如出一辙——“关注点分离”。我们在软件编程的过程中,主要几个关注点:需求设计实现

        变红:写一个让程序运行失败的单元测试用例,它是对一个小需求的描述,只需要关心输入输出,这个时候根本不用关心如何实现。需求

        变绿:专注实现当前需求,不关心其他需求,也不管代码质量是多么惨不忍睹。(实现

        重构:既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,借鉴《重构——改善既有代码》中的手法一并消除它们,让代码变成整洁的代码。(设计

督促写出整洁可用的代码

        Kent Beck在《测试驱动开发》一书中,就开宗明义地提出:TDD的所追求的目标是 Clean code that works(代码简洁可用)。Code that works: 代码首先必须可用——单元测试。Clean code: 代码应该尽可能简洁——重构

        在编写单元测试用例之前,你需要进行 需求分析、考虑程序的使用性、可测试性。并且你只能填充刚好让用例通过的程序逻辑代码,从而有效避免了过度设计,软件代码做到了 “可用”。正因你频繁地重构单元测试用例、程序逻辑代码,软件代码做到了 “整洁”

协助达成质量内建的目标

        采用UTDD的第一步是转变思维——测试左移,将测试用例分析设计和实现前移编写代码之前。是一种极大程度的测试左移,那是不是能极大程度享受测试左移带来的好处?理想情况,你所写的每一行程序逻辑代码,都是为了通过单元测试用例换而言之,你所写的每一行程序逻辑代码,都通过了单元测试用例

默默编制一张系统保护网

        采用UTDD进行程序实现的同时,无形中也在为软件系统编织一张保护网(单元测试用例)。当有新的变化需求重构代码时,你可以大刀阔斧地去做,因为有张保护网替你兜底(回归测试)。即使时过境迁,这张保护网依然能有效地保护系统的边界,能避免让你的系统成为“烫手的山芋”

4.4.2.潜在风险

凡事都有两面性,UTDD的引入也是有代价和风险的。

  1. 学习成本:团队人员初学UTDD,团队推广实践UTDD都是有成本的。
  2. 开发成本:开发人员需要编写、维护测试用例,所带来的时间、工作量成本。
  3. 潜在问题:Mock的大量使用导致很难测试业务价值。若没能在宏观上把控“做正确的事”一原则(ATDD),只是追求“把事情做正确”会掉入“虚假的安全感”陷阱(UTDD)。

5.结语

项目从始至终,我们最核心的诉求是,实现我们的朴素目标——“更好更快地开发出更贴切业务、更稳定、易维护的软件”。项目无论是采用领域驱动设计,还是测试驱动开发,亦或是敏捷实践等方法论,都仅仅只是一种保障手段。所以,我们在项目开发工作中,万万不可一味追寻各种手段,却忘记了初心,最终掉入了本末倒置的陷阱中。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值