[译] 单元测试之迷思 (摘要)

[译者按] 此文为 "Unit Test Fetish" 一文的摘要。因为读到此文之前,俺只是在实践中模糊地发觉和秉承此观念,只是隐隐觉得单元测试并非改善工程质量之良方,也曾用邮件与异地的同事激辩过单元测试之实质作用,但并未找到会心一击直接KO对方,这其实也说明俺还没点到问题的实质。直到读到此文俺才强烈共鸣,作者把俺没有想通想透的东西,用浅显的话解释得非常清楚,俺是边读边与自己的想法一一印证,阅读带来的愉悦感无逾于此。择要编译于此,这样更多的同学也可从中受益。


正文

我听说,现在有些同学克制不住自己写单元测试的欲望,根本停不下来。要是你也是这么想的,那我建议你花几分钟看看下面这些“不该写单元测试”的理由:

a) 从程序员的投入产出比来说,单元测试与端对端测试(end-to-end test)相比,有(若干个)数量级的差距。

一个端对端测试对整个代码库的覆盖率差不多是这样的:
Image Title

而一个单元测试对整个代码库的覆盖率差不多是这样的:
Image Title

拿刷墙来打比方的话,我们应该总是拿最大号的刷子把墙刷个七七八八之后,再用小号的刷子补一下间隙,勾勒一下细节。如果事无巨细地要求提供单元测试,那实际上就是拿小号的中国毛笔来刷整个屋子的墙。

[Gu Lu] 这里俺稍解释一下什么是端对端测试。End to End Test 是用来检查程序的主要流程是否正常工作的一种自动化测试手段。说白了,如果是游戏的话,就是运行某个脚本或命令把游戏自动跑一遍,看看主要模块是否能正常工作,跟冒烟测试基本是一回事。着眼点主要在于程序在真实的生产环境(而非单元测试所惯常使用的 faked/mocked 等测试环境)下的运行状况。

b) 端对端测试可以测试关键路径,单元测试做不到这一点。

正如刚刚提到的,端对端测试模拟了真实情境下的运行。因此,能够成功地运行端对端测试,通常意味着产品某种程度上是可用的。

可是,如果只有一堆单元测试,那最多只能说明对应的一堆零件能正常工作,而拼到一起后的情况是未知的,说不定连最简单的任务都无法完成。

的确,单元测试能保证一个组件考虑到各种边边角角的情况,不过用户通常更关心的是正常流程下能不能用。如果正常流程下都会出问题,这就连产品也算不上了。如果程序在正常流程之外的一个很特殊的情况下出问题,那么通常稍晚些修复也问题不大。

c) 单元测试让内部的架构僵化。

假设你有三个组件 A, B 和 C,全部写有完备的单元测试。现在你觉得架构出现了一些问题,不能适应新的需求了,希望能做一下重构,把 B 拆开,打散到 A 和 C 里面去(B提供的接口都去掉了,A 和 C 的接口都改变了)你会发现所有相关的单元测试都废了,有少量的代码还能打捞出来重用,不过大部分肯定是要重写了。(因为新代码的思路,目的,使用方式,调用接口和内部实现都变了)

完备的单元测试会使产品产生对内部变化的抗拒。 (赞此句) 有经验的程序员在考虑为系统重构预留时间时,如果需要把整个测试套件的重构都考虑进去,通常就会下意识地把本应重构的任务放到“不值得做”那一栏去了。

d) 有些东西是无法做单元测试的。

[Gu Lu] - (本段原文已略) 这里作者举了个协议解码的例子,意在说明(有时)单元测试本质上等于在测试 1+1=2。对某种协议的实现而言,真正有价值的测试是测试两个实体是否真能用此协议去沟通,而不是对着 spec 照葫芦画瓢出一堆等价的单元测试。 

e) 有些东西没有明确严苛的接受标准(acceptance criteria)。

[Gu Lu]- (本段原文已略) 这里作者拿 GUI 为例说明有些东西是靠感觉的,是强调用户体验的,这类东西是没法(也不应该)使用逻辑判断来断定其为 Passed 还是 Failed 的。单元测试的焦点在于验证逻辑的有效性,而整体的交互式测试才能有效地协助开发者发现这类问题。

f) 单元测试的覆盖率很容易被用来衡量代码的整体质量,而这一点是一个危险的信号(你应该对其感到畏惧)

如果你是在一个层层汇报,等级明确的传统公司工作,那你就要小心了!
(在这种公司里),项目的进度会被一级一级地上报,(直至做策略性决策的人手中)。
而软件开发从来都是高度弹性的,很难被度量的活动。
程序员写了多少行代码,QA发现了多少bug,都无法作为衡量其工作表现的指标。

现在有了单元测试在手就好办了——汇报单元测试的数量和代码覆盖率就行了。

可是这其实是个坑:一旦开始把单元测试的覆盖率作为汇报的指标,程序员实际上就受到一种压力去提高这个指标,目标天然就是尽可能接近100%的代码覆盖率(就好像查错的天然目标就是把 bug 数降到0一样)

可是正如前面所说,完全的单元测试覆盖就是高品质的代码吗?这是很可疑的。

“把单元测试覆盖率作为一种指标去汇报”向组织的管理者和决策者提供了虚假的信心,从而导致错误的局势判断和决策。

如果不幸身处这样的组织和制度之中,是很难想到对策与之相抗的。嗯,有条路倒是可以试试——把单元测试的覆盖率搞得足够低,这样就没人会好意思向上级汇报了……(-_-!)

g) 对于那些较复杂的,有明确而严格的行为定义的,有一堆 corner case 的计算任务而言,单元测试还是很有用的。

如果徒手实现一个红黑树,没有完备的单元测试是很难做到的。
不过,说老实话,你经常要去实现一个红黑树吗?

对那些源源不断产生的一批批单元测试而言,究竟有没有一个合理的,有效的理由呢?

还是仔细想想吧,(这里的讨论)没准能帮你节省点儿无谓的工作量。

(正文完)


原文链接:Unit Test Fetish (Martin Sústrik, Jun 4th, 2014)

[Gu Lu] 单元测试并非一无是处,此文的价值在于让开发者更清醒地看待其不足。我自己就感到,自己写过的一些测试基本上就是在测1+1=2,没有体现出测试代码真正的价值。择要摘此文以自省。
另,出于效率考虑,本文省略了部分原文内容(已注明),感兴趣的同学,请自行前往原文对应处查看。

[2014-07-19] Update:

wingc 同学在微博上提出不同看法,俺和回复一起转过来以供对照 :)

@wingc:
...单元测试最基本的功能其实是test driven development和prevent regression最直接最快的手段。文中都在说单元测试如何废,却没有谈其最有用的地方。论点A/B我赞成,C/D/E若把"UT"换成"E2E"一样说得出一堆E2E没用的地方来,F嘛根本就不是UT本身的问题。

我的回复:
C 中明确指出“内部的”架构,而e2e是不关心内部实现的;D 只需注意“真正有价值的测试”一句即可;E 中的大部分确实不是测试能解决的问题;F 是不是 unit test 本身的问题,我觉得不是重点,重点是揭示运用时潜在的风险哦 :)

至于单元测试最有用的地方,最后一条 G 应该是一个解释。

[完]
Gu Lu
[2014-07-19]

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。