前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单元测试与重构

单元测试与重构

作者头像
京东技术
发布2021-09-24 17:34:09
7810
发布2021-09-24 17:34:09
举报
文章被收录于专栏:京东技术

Tech

导读

本文通过讨论测试的必要性以及对比“蛋卷“和“金字塔”两种测试模型,得到越底层的测试应该写得越多的结论,从而得出单元测试的重要性。之后介绍了较为流行的测试驱动开发和如何写好代码,最后介绍了重构相关知识。

通过本文可对单元测试的重要性加深印象,对单元测试即是开发工作中的一个重要环节加以理解。并对如何提升代码质量和写好单元测试提供了扩展学习资料。

01

谁在做测试

开发人员肯定对“测试”工作的必要性不会产生疑问,但是对于“测试”由谁来做这个问题,相信很多人的第一直觉会是“测试不就应该是由测试人员做的事吗”。

事实上,开发人员在交付测试之前都会进行一些基本的测试,确保提交的代码是无误的,尽管你可能不认为这个是在做“测试”工作。

作为测试人员,其实只能站在系统外部做功能特性的测试,而软件是由诸多内部单元和模块所构成的,外部测试只能保证功能的准确性,但其实很难覆盖所有的分支和流程。

试想一下,你购买的一部分汽车,所有的零部件都未经过测试,只有组装完成后由质检员试开一圈,然后交付给你,你会购买这部汽车吗?但是,在软件工程当中,这种场景时时刻刻就在身边发生。

重要概念:

软件开发的成本会随着软件开发阶段逐步增加,也就是说尽早发现问题的修复成本是最低的,能在需求阶段发现就不要拖到开发阶段,能在开发阶段发现的就不要拖到测试阶段,如果上线后才发现问题,那很可能会演变成一场生产事故。

重要思想:

质量内建(Build quality in),其思想大意为,产品质量不是检测出来的,而是从它诞生的那一刻起就已经在那了。对于软件开发来讲,内建要求开发人员做好软件开发每个环节,尽早预防,以降低缺陷出现后的修复成本,要减少对创可贴式的补丁(hotfix)的依赖。更为理想的情况是软件质量贯穿开发的全流程,从需求开始的每个环节将“测试”纳入考量,产品经理确定验收标准,开发人员交出开发者测试。

所以,对于开发人员来说,只有在Coding时把Test也做好,才有资格说交付了高质量的代码,即测试也是开发人员工作中不可分割的一部分。那些在说因为需求比较紧急,所以没来得及做测试的开发人员,有必要定义一下开发阶段的DoD了。

02

测试模型

可以将测试分成以下几个部分:

1、关注最小程序模块的单元测试

2、将多个模块组合在一起的集成测试

3、将整个系统组合在一起的系统测试

以上这些不同类型的测试由上到下,覆盖面越来越高,那么这些不同的测试,开发人员应该怎么搭配更合适呢?

1. 蛋卷模型

一种方法认为,既然高层次的测试覆盖面广,那就多写高层测试,比如系统测试;对于高层次无法覆盖的场景,再由低层次的测试进行补充,比如单元测试;这样就形成了下面这种测试模型:

图一 蛋卷模型(图片源自于网络)

其实这就是很多团队目前的现状,这是一种费事费力的模型。

2.金字塔模型

相比于蛋卷模型,金字塔模型是目前的行业最佳实践。(该模型参考Martin Fowler 的博客TestPyramid)。

想要理解金字塔模型,就要理解不同层次测试间的差异,越是底层的测试,牵扯到相关内容越少,而高层测试则涉及面更广。比如单元测试,只关注一个单元,开发完成即可进行测试;而集成测试则是要把好几个单元组装再一起进行测试,测试通过的前提就是每个单元都正确;系统测试则更复杂,集成好所有模块和单元后,甚至还要维护好基础数据才能进行测试。另外,涉及的模块或单元越多,当其中一个发生变化时可能所有的高层测试都会牵涉其中,复杂度进一步提升,定位问题也会比较复杂。反观低层次的测试,因为涉及内容较少,更容易写测试,一旦出现问题,也比较容易定位。

图二 金字塔模型(图片源自于网络)

所以,测试金字塔的重点就是越底层的测试应该写得越多。那么为什么一些开发人员的单元测试写起来这么难?这个问题后面会有答案。

03

测试驱动开发

什么时候写测试?

对于这个问题,大部分开发者会说“肯定是写完代码之后再写测试”,这么说没有错,但是既然测试是程序员要做的工作之一,那么能不能先写测试再写代码呢?开发者脑海中肯定是浮现了一个词TDD(Test Driven Development),TDD 就是先写测试后写代码,然而这个理解是错误的,先写测试后写代码的实践应该是“测试先行开发(Test Frist Development)”,虽然只差了一个词“驱动”,那这两个词之间有什么区别呢?想要理解测试驱动开发,就要理解什么是“驱动”,也就是TFD和TDD的差别。

图三 测试驱动开发

如上图,测试先行开发和测试驱动开发,在第一步先写测试,第二步写代码使测试通过,这两步是一样的,区别点在于第三部分“重构”,也就是说测试驱动开发在开发完成,测试跑通之后,还需要再次回到代码上“重构”,因为刚刚只是让代码跑起来了,设计上还有可改之处,新增代码往往存在很多“坏味道”,而重构则是消除坏味道的手段,一旦有了测试,就可以大胆的进行重构,因为任何错误都可以很容易的被捕捉到。

平时总能听到“这段代码是有问题,但是现在不敢改”、“这段代码不敢动,所以复制了一份在此基础上进行增改”等等这样的话,这些问题总归来讲,就是没有做好单元测试。

测试驱动设计

很多开发人员排斥单元测试,常见有两个理由:需要“额外”的工作量,时间不够;代码太多不好测。第一点,上面已经提过,测试就是开发人员工作的一部分;第二点,细想一下会发现,其实说的是代码已经写完了,需要后补测试。

如果把“先写代码,后补测试”转换成:先写测试,写代码是为了让测试通过,写出的代码天然具备可测性,是不是就变得简单了呢?

在实际开发工作中,经常能见到长达100行及以上的函数/方法,这种代码绝大部分开发者会说不具备可测性。如果写代码时时刻想着可测性,是为了让测试通过,开发者再写这么长行数的代码都难。了解编写可测试代码的思路,即便不做 TDD,依然对改善软件设计有着至关重要的作用。所以,写代码之前,请先想想怎么测。

至于如何写好代码,可参考《代码整洁之道》。

04

简单测试

上面遗留了一个问题,为什么开发人员的测试写起来这么难呢?这个问题和开发者写出来长达100行的方法有着直接的关联,因为它太过于复杂了。只有将复杂的测试拆分成简单的测试,测试才有可能做好。

《代码整洁之道》有个很重要的原则:只做一件事。函数、类、模块,都全神贯注一件事。软件设计的许多原则最终都会归结为这句警句 。当定义的方法明明是getXXX,却改变了入参的属性;当定义方法时即有返回值,又改变了入参时;当在一个for循环里,改变两个值.....都在违反这一重要的原则。

既然测试也是用代码写的,那么如何保证测试代码的准确性呢?只有一个方法:把测试写简单,简单到一目了然,不需要证明它的正确性。

一种测试常见的坏味道是没有断言!这种测试就从来没失败过,一看代码竟然是print,这种测试最多也就能证明你曾经debug过这段代码。另一种是有断言,通常是assert 不等于0,true/false一类,看似没问题,但是如果真失败了,需要把调用代码读一遍,甚至debug才能定位到错误。还有一类测试,只能在编写测试时正常执行,后续其他开发人员将代码clone下来,无论如何也不能再次正常运行。

《单元测试之道》中总结道,好的测试应该遵循A-TRIP原则:

  • Automatic,自动化
  • Thorough,全面,应该尽可能用测试覆盖各种场景
  • Repeatable,可重复的
  • Independent,独立的
  • Professional,专业的

另外,关于如何测试,也有个[Right]-BICEP缩写:

  • Right – Are the results right? 结果是否正确?
  • B – are all the boundary conditions correct? 所有边界条件都是正确的么?
  • I – can you check the inverse relationships? 能否检查一下反向关联?
  • C – can you cross-check results using other means? 能够使用其他手段交叉检查一下结果?
  • E – can you force error conditions to happen? 是否可以强制错误条件产生?
  • P – are performance characteristics within bounds? 是否满足性能要求?

其中边界测试有个CORRECT的缩写:

  • Conformance(一致性):值是否和预期一致。可以理解为当输入并不是预期的标准数据时,被测试方法是否可以正确输出预期结果(或抛出异常)。
  • Ordering(顺序性):值是否像应该的那样是无序或有序的。
  • Range(区间性):值是否位于合理的最小值和最大值之间。
  • Reference(依赖性):代码是否引用了一些不在代码本身控制范围之内的外部资源,当这些外部资源存在或不存在、满足或不满足时,代码是否可以产生相应的预期结果。
  • Existence(存在性):值是否存在(是否为null、0、在一个集合中)。测试方法是否可以处理值不存在的情况。
  • Cardinatity(基数性):是否恰好有足够的值。这里的基数指的是计数,测试方法是否可以正确计数,并检查最后的计数值。
  • Time(相对或绝对时间性):所有事情的发生是否是有序的、是否在正确的时刻、是否恰好及时。与时间相关问题有:相对时间(时间上的顺序)、绝对时间(消耗的时间和钟表上的时间)、并发问题。例如:方法调用的时间顺序、代码超时、不同的本地时间、多线程同步等。

05

重构

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."

任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员会编写人类可以理解的代码。

本质上讲,重构是为了改进已有软件/代码的设计,使软件更易于维护;也就是说,在不改变系统/代码的外部行为前提下,以改进其内部结构的方式改变软件系统的过程,使其更易于理解且修改成本更低。

1、何时重构

《重构》中提到的三次法则,大意为:事不过三,三则重构。结合到日常开发工作中,可以在以下几个节点进行:

  • 添加功能时一并重构
  • 修复BUG时一并重构
  • 代码评审时一并重构

是不是有些情况,也不适合重构?通常来讲,如果重构的现有代码过于混乱,重构的成本过高,甚至重来要比重构还要容易则不再适合重构。另外,应尽量避免在临近最后时间点时进行重构,以免推迟计划,这种情况更适合将重构当成一项新的任务进行。

2、常见坏味道

  • 重复代码
  • 大方法
  • 大类
  • 方法参数列表过长

对于如何做好重构,可参考Martin Fowler著作《重构》。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 京东技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档