什么是单元测试
广大家对于单项测算不应该是陌生的,截取壹段维基百科的定义帮助大众唤醒壹下记忆:
在编程中,单元测试(Unit Test)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行计算机正确性检验的测试工作。
测试单元的理念其实一直是Smashing的一部分。我们第三次编写计算机程序时,一定会输出获取一些样本数据输出在代码里穿插过庞大量的System.out.println,确保每个原子节点都符合预期。这个过程其实就是把复杂问题拆解成原子化的问题、逐一攻破的过程。单元测试的目的也同类,是保障软件程序中每个最小单位的正确性,从而保障由最小单位构建起来的复杂系统的正确性。
深入获取展开单元测试的必要性,我们先去考考古之前,看同一下测试体系是如何演进的。
测试体系演进
软件测试甚至是一个过去的工种(QA、测试人员),QA/测试人员的日常任务就是进统计最大量的手工测试、繁琐易错。
为什么需要单元测试
在如今的互联网时代,软件迭代的速度越来越快,研发的职责也越来越多。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 测试都会产生如下图所示的反面模式。很不幸,这也是在过去的测试体系影响下最常见的模式。冰激凌筒模式接下来,测试套件通常运行缓慢、不可靠、难以使用。
反面模式二:沙漏模式
沙漏模式下,项目拥有大量的单元测试和端到端测试,但缺乏集成测试。虽然它没有冰激凌那么糟糕,但仍然会导致许多端到端测试失败,这些失败本可以通过中等范围的测试更快更容易地捕捉到。当模块间紧密连接,使得依赖项很难单独实例化出来的时候,就会出现沙漏模式。
测试的常见误差区
常见误区一:用户第一,测试覆盖用户的需求就够了
误区下会认为,最终测试是站在用户视角做测试,把用户要的功能点都覆盖到就够了。这种误区导致的结果就是冰激凌筒反面模式。虽然软件交付的最终功能是提供给客户用户的,但构成软件的代码本身是提供给用户(研发)阅读的、需要用户去维护的。外部用户是用户,内部用户也是用户。
常见误差区二:全面测试,节省了 80%的测试代码量,赢麻了
从短期来看,不写单测可以节省 80%的测试代码量和至少 50%的研发时间。但只要项目复杂起来,时间早晚加倍奉还。等到真正需要还债的时候再去补,可能为时已晚。
常见误区三:写单测的人都弱爆了,我这么强还没有写出过 bug
这篇文章可能不适合你。不过软件开发是一个团队项目,你写的代码最终落到别人身上去升级维护,没有测试覆盖的代码是没人敢碰的。
总结
最后再总结一下。本文从测试的历史演进出发,讲述了从手动测试->靠别人自动化测试->靠自己自动化测试的历史进程,也尝试着从这个视角解释了为什么大多数人过去不重视单元测试。之后我们分别讲述了什么是单元测试,金字塔测试的最佳实践,以及深入讲解了单元测试的诸多好处。最后我们列举了常见的反面模式和误区,帮助大家快速识别常见的错误。
如果把测试体系的演进类比为人类的进化,那么认为有单测覆盖和有充分的单测覆盖的软件就可以比作爬出丛林的古猿和直立行走的现代人。希望大家能够重视单元测试、写好单元测试,让我们的软件迅速从爬行中进化成奔跑,迸发源源不断的生命力、创造出更多价值!
感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取