我一直在质疑单测的作用,无法理解单测到底对 RD 有什么实质的收益。
举例来说,我花 20 分钟写了一段 80 行的代码,这时,我往往需要 40 分钟,写 200 行冗长、累赘、丑陋的 mock 代码去覆盖涉及的各个场景。很多时候,写单测比写业务逻辑还要费事—— mock 返回值太费劲了,尤其是 mock 一些静态或私有方法。而单测能带来什么呢?多数情况下,单测都能通过,而如果单测无法通过,只有两种可能性。毕竟单测只测试一个类,甚至只测试一个方法。如果连这种不和外部交互的代码你都能写错,一,你根本就没有理解需求,你写的代码是一团浆糊,你自己都不到自己在干什么;二,你犯了非常低级的失误(但不得不承认,最优秀的 RD 也会犯低级的失误)。需求没分析清楚,你该找 PM、翻 PRD;犯了低级失误,也犯不着写这么多“无用”的代码。在集成测试环境,用浏览器、Postman、模拟 RPC 很快就能测出这些低级错误,打个断点就能找到问题并修复,效率比写单测高太多了,也能测到很多单测根本无法涉及的地方。所以我一直认为单测是投入产出比是非常低的。
以上还只是针对理论场景,在实际的编码开发中,单测更加显得鸡肋。打开任意一个项目,相当一部分的单测根本就没有 Assert,换言之,这些单测永远能过,除非代码运行中途抛了个 Exception。这些单测,除了能提高单测覆盖率,没有任何实际作用。老实说,为了应付单测覆盖率门限,我也写过很多没有断言的单测。这就把单测放到了了一个相当尴尬的境地。我甚至可以基于以上的论据发表一个暴论:单元测试根本就不是测试!绝大多数的缺陷,都发生在系统和系统、模块和模块之间,只对系统的最小单元进行测试,对降低系统风险的作用微乎其微——这就相当于对桥梁质量进行验收时,只看每一根钢筋、每一颗螺丝是否符合质量标准,而不顾桥墩的设计、受力结构的分析、承重能力的测试。
《软技能2:软件开发者职业生涯指南》一书的作者,是从 QA 转岗到软件开发的,他对单元测试和 TDD 的理解是比较独到且深入的。他也基本上认为“单元测试不是测试”,“当你运行单元测试程序的时候,你一般不会发现有些代码没有正常工作”。但单元测试是良好的代码规格说明书。通过单测,你规定了对应的代码在指定的输入下,该有怎样的表现或者输出。而单测的巧妙之处在于,它和纸质说明书不一样,它是可执行的。这也就意味着,在代码发生了变动之后,你可以通过执行单测来判断它是否还遵守着之前的规格说明。总结而言,单测的优势如下:
单测能够进行基本的回归测试。单测能够保证在代码中引入的新变更不会破坏原有的功能,如果之前的单测明确指定了规格说明的话。可以把单元测试想象成你在幼树上看到的那些小支架,它们的作用就是确保小树长得笔直高大。你栽种了的一棵笔直的小树,并不意味着随着时间的推移它不会长弯。你的代码也是一样的。单元测试最初可以告诉你,你的代码就像一棵笔直挺拔、长势喜人的小树,然后它可以帮助你的代码一直保持这种良好状态,即使一些菜鸟级开发者给脆弱的代码带来了一场暴风骤雨。
单测能够提示代码的设计问题。代码的内聚性和可测试性是一对孪生兄弟。我曾尝试过在一个老系统中编写单测,它的逻辑散在各个方法中,同时流程很长,写单测会陷入一种两难局面:要么你只针对一个特别小的局部写单测,要么就要从头到尾 mock 整个流程。前者的收益太小,没什么意义;后者要 mock 的东西实在太多,我在接连 mock 了二十多个方法之后还是放弃了。分析原因,这个系统的代码随着需求演变,内聚性变得非常差,一个类根本完不成一件事情,代码像过程式编程一样散在各处,看得都费劲,更别说写单测了。所以说,单测可以提示设计中可能存在的问题,保证自己的设计遵循了“一个方法干一件事”、“一个类负责一类事”,高内聚低耦合,否则会恶心到写单测时的自己。
单测帮助我们正确理解需求。这点可能有些抽象。测试用例一经编写,要么通过,要么失败。测试以明确无误的方式规格化地定义了在某种情况下程序应该发生的事情。因此,单测确保我们在实现代码之前充分理解我们要做的工作是什么,进而确保我们“正确地”实现了它。说的明白一点,如果你坐下来写单测,但却不知道应该测试什么,这就意味着你对代码预期的功能还不甚了解,你需要问更多的问题。
最佳实践:
至少,要在编码时同步写单测用例。是否要在编码开始之前就编写单元测试用例是一个很有争议的话题。但我确信,如果在编码完成后再写单测用例,单测用例就完全没有意义——你早已在大脑里完成了编码的规格说明。也正因为如此,我们才会觉得单测是多此一举,认为单测对 RD 而言没有什么收益。我们都已经在代码里注意到的点,为什么要在单测里再写一遍?在事后写单测,它既不能提示你的设计问题,也无法保证你理解需求,只能略微帮助回归测试。它的用处就是耗费你的时间,为你增加一点代码提交量。说实话,事后写单测,不如不写。
单元测试不能一直成功。也就是说,单测一定要有有意义的断言。否则,这就不是单测,执行它也只是让机器空转,没有断言的单测只是为了覆盖率好看的 KPI 产物。
若时间有限,针对典型应用场景写一个用例。只写最常见的一个用例也比不写用例要好,它带来的收益(回归、改进设计、理解需求)超出了它的时间成本。此外,单测也是会越写越熟练的。
最后可以总结,单元测试并不是真正的测试。但祸兮福之所倚,它同时具备了测试并不具有的价值,这些价值对开发人员非常重要。让集成测试、验收测试去做真正的“测试”工作吧,单测的价值,从来就不在所谓的测试上。