[译者按] 此文为 "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

Comments
Write a Comment

Tags

随笔   游戏开发   Programming   C/C++   优化   Unity   C++   知乎   游戏设计   比特币   Unity3D   区块链   软件开发   Bitcoin   引擎设计   系统架构   Production   idtech   中国文化   加密货币   项目管理   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Blockchain   Lua   Blockchain Computing   Oculus   GDC   渲染   VR   PerfAssist   Unity MemoryProfiler   BCH   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   测试   中间件   Game Engine   新手引导   区块链游戏   Methodology   CI   命令行解析   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   笔记   golang   图形编程   多线程   ETH   Bitcoin Cash   cppcon14   Bitcoin SV   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   量子计算   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

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