前⾔
刹车是降低了⻋速还是提升了⻋速?我们通常认为写单测费⼒动作、耽误研发进度,仿佛在给项⽬ “踩刹⻋”。大规模家形状带着这个问题往下看,详细聊聊为什么单元测试可以让软件开发跑得更快。
什么是单元测试
广大家对于单项测算不应该是陌生的,截取壹段维基百科的定义帮助大众唤醒壹下记忆:
在编程中,单元测试(Unit Test)⼜称为模块测试,是针对程序模块(软件设计的最⼩单位)来进行计算机正确性检验的测试⼯作。
测试单元的理念其实一直是Smashing的一部分。我们第三次编写计算机程序时,一定会输出获取一些样本数据输出在代码⾥穿插过庞大量的System.out.println,确保每个原⼦节点都符合预期。这个过程其实就是把复杂问题拆解成原⼦化的问题、逐一攻破的过程。单元测试的⽬的也同类,是保障软件程序中每个最⼩单位的正确性,从⽽保障由最⼩单位构建起来的复杂系统的正确性。
深入获取展开单元测试的必要性,我们先去考考古之前,看同一下测试体系是如何演进的。
测试体系演进
软件测试甚⾄是一个过去的⽴的⼯种(QA、测试人员),QA/测试人员的⽇任务常就是进统计最大量的⼿ ⼯测试、繁琐易错。
2000年代初以来,软件业的自动化测试已经发生了巨大的变化。为了应对现代软件系统的规模和复杂性,人们逐渐发展起来开发人员驱动的自动化测试实践。可以摆脱⼿动测试的繁琐,利用软件来测试软件。但过去的实践仍然留下了必然的影响,软件测试还是同一个⽴的⼯种,过去的QA演进成了SDET(测试中的软件开发工程师) ),我们虽然已经发展到了能够使用⼯具了,但我们也只是会使用⼯具的猴子⼦。为什么这么讲?因为这种研发/测试分离的模式本身就留下了很多问题。
当研发和测试时是两个岗位时,交付的边界是软件整体的功能性(功能需求)和可使用性。研发只要保证软件整体上功能完成、可使用就可以了,测试也集中在集成测试和端到端测试但软件是由⽆数个最⼩单位构成的,在这种体系下人们会关注最⼩单位的质量、是否唯一可测可演进,难免“⾦最终⽟其外,败絮其中”。
基于种种弊端,⾕歌、微软这些对研发质量⾮常重视的公司都在从SDET的2.0时代过渡到一体化的3.0时代:微软在2015年去掉SDET⼯种,在陆奇改造的Bing中率先提出“组合工程”的概念;⾕歌也将SETI替换成EngProd(工程生产力),专门负责测试平台和⼯具的搭建,不负责具体的业务逻辑测试。
为什么需要单元测试
在如今的互联网时代,软件迭代的速度越来越快,研发的职责也越来越多。DevOps的理念是“you build it, you run it”,研发/测试合⼆为同一的趋势也可以理解为对“你构建它,你测试它”的要求。当研发要对自动编写的代码质量和测试负责的时候,好的测试实践就必不可少了。
测试⾦字塔
就像盖楼需要从打地基、竖直防水、灌⽔泥层往上构建一样,测试也有类似的测试⾦字塔架构。下图出自《Software Engineering at Google》的测试章节,总结了Google在一天测试的最佳实践中。我们可以看到测试⾦字塔由三层构成,最底层就是单元测试、占⽐80%,是软件系统的基础。再往上是集成测试和最终测试,分别占15%和5%。因为从下往上占⽐逐层缩减,因此被称为测试⾦字塔(跟盖高层楼一类)。⾕歌推荐的这个⽐例是多年实践出来的结果,意在提升研发的效率(productivity)并提升对产品的信度⼼(产品信心)。
⾦字塔的核心理念之壹就是“Unit Test First”,每个软件项⽬⾥的第一个测试项目应该是单测(TDD甚⾄认为第一个代码就应该是单测),⽽且唯一一项⽬⾥占⽐最高的测试也应该是单测。
优秀的软件单元测试
业界都把单元测试放在这么重要的位置?“抓放大⼩”,只写端到端测试不⾹吗?这就是为什么⾥我们来展开讲单测的好处。
提升调试效率
单元测试是软件⼯程极佳的基础,因为它们快速、稳定,并且极大地压缩了问题范围,提升了故障诊断的效率。
- 测试更快:单测没有其他外部依赖,跑的快,可以提供更快的反馈环,更快的发现并修复问题。
- 测试更稳定:同样因为0依赖,单测相⽐于其他类型的测试更稳定,不会受外部其他模块的不兼容变更影响。因此单测也是最能导致开发者信⼼的测试类型。
- 问题更容易定位:单测以最大⼩软件单位为边界,生长问题可以缩⼩定位范围。相⽐下方,越是⾦字塔上层的测试类型,定位问题的难度越庞大。复杂的端到端测试涉及群体的模块,需要一致性队列查定位问题。
提升代码质量
代码是写给别人看的,好的代码应该是易读、易改、易维护的。写单测的过程其实就是吃自主⼰代码狗粮(dogfood)的过程,从用户/研发视⻆去使用自主⼰的代码,帮助我们提升代码质量。
- 好的代码是容易测的:市场上很早就提出了圈复杂度(Cyclomatic Complexity)的概念,利用来简化一个模块替代结构的复杂程度,其数量上行走独⽴路径的条数,也可理解为覆盖所有的可能情况最少使使用者的测试使用例个数。圈复杂度极大说明程序代码的判断逻辑复杂度,可能质量低,且难于测试和维护。因此代码好的一件事定是圈复杂度低的,也是容易测试的。
- 修改迭代演进:没有什么软件是不变的,好的软件系统应该是易于演进的。单测覆盖更高的项⽬模块更原⼦化,边界更清晰,更容易起来。单测覆盖更所有的项目⽬重建的⻛险也相对更⼩,正好一个没有单一测覆盖的复杂项目⽬是没人敢碰的。
- 更优质的设计:前面也提到过,好的单测能够提升代码的质量。如果某个研发需要给自主⼰的代码写单测,他关注代码的层级分割,减少过⻓ 、圈复杂度达到更高的方法。下面的例子⼦就是壹段没有单测的代码的认知复杂度值(可以理解为圈复杂度的壹个改良版,从代码是否容易理解的⻆度简单),超标了⾜⾜三倍。现在回过头来想补单测,脑子都最大了。
提升总体研发效率
⼑不误砍柴⼯,提高质量、完善速度的单测可以提升研发质量和效率,加快项目⽬总体交付。这句话乍一看是反常识的,写单测往往⽐写实现逻辑要更这也是多数家不写单测最常⻅的理由:“项⽬赶进度,来不及写单测”。如果我们的项⽬生态命周期⽉计算的,写个原型很快就下线了,那写单测不投资回报率不提高。但是阿⾥有很多to B的业务,提供给用户的能力⼒都是以年计算生命周期的,提高质量的代码随着时间的推移,投资回报率将会越来越提高,具体体现在以下几个方面:
- 减少调试时间:上面提到了上述提到的提升调试效率的原因,这⾥不再重复。凑够了更高的单测覆盖可以节省调试所耗费的时间,凑够了⾜测试覆盖的项⽬本身bug数量就会较少。举个现实中的例子⼦:某团队由于历史上⽋的种种债务,基本全靠最终测试,几⽆单元测试覆盖。造成的后果也⾮常严重,团队oncall的同学> 50%的时间都是在修复各种奇怪的bug,无法投到宝贵的精⼒到架构升级等⻓期更重要的项⽬上。
- 增加代码变更的信⼼:前面提到没有测试覆盖的代码没有人敢碰,有充⾜单测覆盖的代码可以显着提升改造代码的信⼼和愿望。再给大家举个例子⼦:我在 Google 总部⼯作过几乎⼗年之前就已经获得了阿⾥。如果你在 Google⼯作过就能发现,你的代码经常会收到无关团队成员发起的代码更改。绝大多数情况下这些都是同学们自主制定的大规模自动化重构(mass refactor),⽐如果看你的Java代码没有使用Builder模式,就会帮助做重构个(Google⾥有大规模自动化动化⼯具简化)这些重构⼯作)。我们迫切渴望不谈,如果是没有覆盖的代码、还是没有相关组的,你敢这么重构吗?我们都希望能有像⾕那样整洁的代码,但没有人敢碰的代码怎么变得更好?
- 提升代码的自主解释性:文本档能够提升代码的自主解释性,让研发效率更高。好的单测其实也可以被提升代码的文本档,通过读取测试可以快速理解代码的用途(参参) ⻅TDD)。单测作为文本文件同时还完美的解决了文本文件保鲜的难题,给开发者提供了一套高档质量、随着代码不断更新的文本文件。
- 更高效的代码审查:并不是所有的问题和设计上的缺陷都通过静态检查发现,这也是为什么需要人⼯代码审查作为代码质量的最后一道防线。在Google,代码审查是代码合并最重要的的一个阶段,因此评审的效率直接影响总体的研发效率。好的单测覆盖能够减轻评审人的负担,让他们把精⼒投获得到更重要的部分(⽐如代码设计)。
- 更频繁的开发版:敏捷开发的持续集成、持续部署的前提就是全餐、提高质量的自主动化测试。敏捷开发对于研发的提效还没有展开。但光是能够更快速的开发版本身就已经⾮常有价值了。
反面模式和常⻅误区
上面提到了写单元测试的一系列好处和相关的最佳实践。我们还列举了下常见的反面模式和误区,帮助大多数家庭更好地规避类似的错误。
测试的反面模式(anti-pattern)
反面模式一:冰激凌筒模式
只关注用户户视⻆的端到端测试、大规模依赖QA测试都会产出如下图所示的反面模式。很不幸,这也是在过去的测试体系影响下最常见的模式。冰激凌筒模式接下来,测试套件通常运行缓慢、不可靠、难以使用。
图⽚来源:Google 软件工程
反面模式⼆:沙漏模式
沙漏模式下,项⽬拥有大量的单元测试和端粒测试,但缺乏集成测试。虽然它没有冰激凌那么糟糕,但仍然会导致许多端粒测试失败,这些失败本可以通过同一件事套中等范围的测试更快更容易地捕捉到。当模块间紧密连接,使得依赖项很难单独实例化出来的时候,就会出现沙漏模式。
图⽚来源:Google 软件工程
测试的常⻅误差区
常⻅误区一:用户户第一一,测试覆盖用户户的需求⾜够了
误区下会认为,最终测试是站在用户视角做测试,把用户要的功能点都覆盖到就够了。这种误区导致的结果就是冰激凌筒反面模式。虽然软件交付的最终功能是提供给客户用户的,但构成软件的代码本身是提供给用户(研发)阅读的、需要用户去维护的。外部用户是用户,内部用户也是用户。
常⻅误差区⼆:全面测试,节省了80%的测试代码量,赢麻了
从短期来看,不写单测可以节省80%的测试代码量和⾄少50%的研发时间。但只要项⽬复杂起来,时间早晚加倍奉还。等到真正需要还债的时候再去补,可能为时已晚。
常⻅误区三:写单测的人都弱爆了,我⻓这么大还没有写出过bug
这篇文章可能不适合你。不过软件开发是一个团队项目⽬,你写的代码最终落到别人身上⼿⾥升级维护,没有测试覆盖的代码是没人敢碰的。
总结
结束处再总结快速一下。本发送从测试的历史获取⼿,讲述了从⼿动测试 -> 靠别自主动化测试 -> 靠自主⼰自主动化测试的历史进程,也尝试着从这个视角解释了为什么大多数家过去不重视单元测试。之后我们分别讲述了什么是单元测试,接下来的⾦字塔测试最佳实践,以及深入学习讲解了单元测试的诸多好处。最后我们列举了常⻅的反面模式和误区,帮助广大家快速识别常⻅的错误。
如果把测试体系的演进类⽐作为人类类的进化,那么认为我⽆单测覆盖和有充分的单测覆盖的软件就可以⽐爬出一组的古猿和直⽴一组⾛的现代人类类。家能够重视单元测试、写好单元测试,让我们的软件迅速从爬升中进化成奔跑,迸发源源不断的生成命⼒、创造出更多价值!
作者|乐凯
点击立即免费试用云产品 开启云上实践之旅!
原文链接
本文为阿里云原创内容,未经允许不得转载。