“单元测试要做多细?”
这篇文章主要来源是StackOverflow上的一个回答——“How deep are your unit tests?”。一个有13.8K的分的人(John Nolan)问了个关于TDD的问题,这个问题并不新鲜,最亮的是这个问题的Best Answer,这个问题是——
“TDD需要花时间写测试,而我们一般多少会写一些代码,而第一个测试是测试我的构造函数有没有把这个类的变量都设置对了,这会不会太过分了?那么,我们写单元测试的这个单元的粒度到底是什么样的?并且,是不是我们的测试测试得多了点?”
答案
StackOverflow上,这个问题的答案是这样的——
“I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards, but that could just be hubris). If I don’t typically make a kind of mistake (like setting the wrong variables in a constructor), I don’t test for it. I do tend to make sense of test errors, so I’m extra careful when I have logic with complicated conditionals. When coding on a team, I modify my strategy to carefully test code that we, collectively, tend to get wrong.”
老板为我的代码付报酬,而不是测试,所以,我对此的价值观是——测试越少越好,少到你对你的代码质量达到了某种自信(我觉得这种的自信标准应该要高于业内的标准,当然,这种自信也可能是种自大)。如果我的编码生涯中不会犯这种典型的错误(如:在构造函数中设了个错误的值),那我就不会测试它。我倾向于去对那些有意义的错误做测试,所以,我对一些比较复杂的条件逻辑会异常地小心。当在一个团队中,我会非常小心的测试那些会让团队容易出错的代码。
这个回答对TDD似乎有一种否定,最亮的是这个问题是由Kent Beck,Kent是XP和TDD的创造者,是敏捷开发实践方法的奠基人。以致于还有人调侃到——
The world does not think that Kent Beck would say this! There are legions of developers dutifully pursuing 100% coverage because they think it is what Kent Beck would do! I have told many that you said, in your XP book, that you don’t always adhere to Test First religiously. But I’m surprised too.
只是要地球人都不会觉得Kent Beck会这么说啊!我们有大堆程序员在忠实的追求着100%的代码测试覆盖率,因为这些程序员觉得Kent Beck也会这么干!我告诉过很多人,你在你的XP的书里说过,你并不总是支持“宗教信仰式的Test First”,但是今天Kent这么说,我还是很惊讶!
后面还有一些人不同意Kent, 我一下子从这个事中想到了《fight club》里的那个精神分裂者创建了一个连自己都反对的地下组织。呵呵。
其实我是非常同意Kent的,怎么合适怎么搞,爱怎么测试就怎么测试,只要自己和团队有信心就可以了。没有必要就一定要写测试,一定要测试先行。
其它答案
八卦完了,我们还是来认认真真地看看这个问题中其它的其它答案,因为这个问题的也是国人爱问题的问题。
第二个答案:值得借鉴
- 开发过程中,单元测试应该来测试那些可能会出错的地方,或是那些边界情况。
- 维护过程中,单元测试应该跟着我们的bug report来走,每一个bug都应该有个UT。于是程序员就会对自己的代码变更有两个自信,一是bug 被 fixed,二是相同的bug不会再次出现。
第三个答案:给敏捷咨师看的答案
这个答案在说,我们只注意到了TDD中的T,而忽略了第一个D,就是Driven…… bla bla bla… 又这扯这些空洞的东西了,国内的各种不学无术的敏捷咨询师最好这一口了。
第四个答案:致那些什么都要测试的人
如果我们需要测试一个像 int square(int x)
这样的开根函数,我们需要40亿个测试(每个数都要测试)。
事实上这种情况可能还更糟糕,如果有这样一个方法 void setX(int newX)
不会更改其它的成员变量,如:obj.z, Obj.y,那么,你是不是还要去测试一下别的变量没有被改变?
我们只可能测试那些有意义的,确实要测试的案例。
我的观点
我在《TDD并没有看上去的那么美》一文中说过我的观点了,我就不再多说了。我还是把下面这些观点列出来,供大家思考和讨论:
1)我国的教育对我们最大的洗脑不是掩盖事实,而让我们习惯于标准答案,习惯于教条,从而不会思考!敏捷开发中的若干东西似乎都成了软件开发中对某种标准答案的教条,实在是悲哀!
2)软件开发是一种脑力劳动,是一种知识密集型的工作,就像艺术作品一样,创作过程和成品是没有标准答案的。
3)软件的质量不是测试出来的,而是设计和维护出来的。就像工匠们在一点一点地雕琢他们的作品一样。
UT的粒度是多少,这个不重要,重要的是你会不会自己思考你的软件应该怎么做,怎么测试。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《“单元测试要做多细?”》的相关评论
测试的目的不是证明代码正确(也做不到),而是帮助发现错误,包括低级的错误。
编码质量是另一个话题,但再强悍的人也有失手的时候,特别是有 deadline 紧追不舍时。
况且编写代码的人多数情况下不会一辈子亲自维护,写下的代码以后会被很多人修改,变数很多。
如果有时间写测试,我觉得还是尽量去写。没时间或者觉得没必要就算了。
把单元测试的地位抬得太高,作为质量衡量标准就有点过分了,个人愚见。
软件的质量不是测试出来的,而是设计和维护出来的。就像工匠们在一点一点地雕琢他们的作品一样。
但软件的质量要靠测试来验证的
测试有两种目的,其一是检查错误,其二是验收。前者是程序员的工作,后者是测试部的任务——很明显,程序员追求质量的方式,和测试人员是完全不一样的。
这里只讨论程序员的工作。
有句话说的好:写一个软件有两种思路,一种是简单到明显没有错误,一种是复杂到没有明显的错误。前者远比后者更难。
一个好的软件工程师,必然是把自己近乎全部的精力放到“如何设计系统、以使其简单到明显没有错误”,而不是“我有一千八百七十六万个测试用例,这些用例暂时还没有发现什么明显的错误”。
不客气的说,一个测试用例都没有的前者几乎肯定是个优秀软件;有一千八百万测试用例的后者明显就是个彻头彻尾的垃圾。
对程序员来说,做到“简单到明显没有错误”太难,不得已退而求其次,这才只好去写“尽量简单、但偶尔还是有点晦涩”的程序;然后就不得不通过测试来检查晦涩之处有无错误——当然,如果后来通过苦思冥想、翻阅资料,能够一下子以极简的方式解决掉晦涩部分,那当然是最痛快的。
TDD教、UML教还有面向对象教最大的问题,就是搞出种种清规戒律、却逃避甚至排斥“作出简单到明显没有错误的方案”方向的任何努力。而后者才是解决软件质量问题的正确方向。
所以才说:软件的质量不是测试出来的,而是设计和维护出来的。
@xum
非常不错
现实本身就是复杂到没有明显的错误, 所以如果作为对现实的一种映射的代码是简单到明显没有错误, 那么很明显代码是简单到反映不了现实情况.
这种说法就跟老金的”无招胜有招”,老子的”无为而无不为”一样, 听着挺有道理,看着也挺有道理,可就是现实中行不通.
>现实本身就是复杂到没有明显的错误
我不知道你这个结论是从哪得来的。
现实本身,至少从目前的认识来看,牛顿三大运动定律本身基本就可以囊括一切了;更变态一点的需求,可以通过相对论修正(比如GPS。但相对论本身不过是两句话和两个变换公式);至于量子力学,它的修正现在基本用不到——即使用到,它也不过就是一句话罢了。
至于化学,早在上百年前就已经可以归结为物理了。
————————————————————————————
很显然,现实本身是如此简单明了;也正因为其简单,我们才可以自牛顿力学开始,逐步深入,搞出现代文明体系。
牛顿力学的确并非现实的精确表述,在一些极端情况下,它需要打一些补丁。
而目前需要打上的补丁,也不过是为极端情况加上简单的两个修正(广义/狭义相对论),量子力学暂时尚无用武之地。
————————————————————————————
牛顿三大定律是机制;由它构成某种复杂系统则是策略。
——类似软件核心设计的“提供机制但不提供策略”理念一样,谈及策略,就必须具体到某个行业。
以汽车发动机设计举例,其后台机制仍然是牛顿三大运动定律,实现策略是汽缸/活塞/曲轴/连杆/轴承/齿轮:举凡机械,没有什么超越以上构件范围的。
显然,这也就是说,到了发动机上,无非是:基于牛顿三大定律这个库,提供了汽缸/活塞/曲轴/连杆/轴承/齿轮这个新的基础库;而如何利用汽缸/活塞/曲轴/连杆/轴承/齿轮等等机制以实现特定的机械,需要工程师设计另外的“策略”——而这个策略,显然不是汽缸/活塞/曲轴/连杆/轴承/齿轮库所应提供的。
如此反复,层层抽象,每层都很简单;等到了汽车整车上,就是“利用发动机/变速箱/轮胎/车身这个库提供的机制”,以某种“策略”实现特定车型了——显然,这仍然很简单。
正是基于以上思路,现代工业才成为可能。
——————————————————————————————
很显然,正因为你和另外一些人根本理解不了现代科学的根本理念,这才会把它和“无招胜有招”这种扯淡相混淆。
更具体点说,接受了现代科学根本理念的人,自然会把库-机制-策略看做吃饭喝水一样平常,他们自然懂的“怎样才能把软件搞简单”:无非是合理分层、正确抽象、区分机制和策略罢了。
相反,脑子仍然被“道可道,非常道”这种垃圾充满的,他们除了哭“现实如此复杂”,还能做什么?
面对“复杂的现实”,他们要能找到哪怕一丝脉络,那才叫白日见鬼了。
正是这种人,才会去不着边际的吹捧TDD教/UML教以及面向对象教——严肃的后三者的确是利器,但不应被没有自控力的小孩子接触。
会者不难,难者不会,这句话精辟的描募出了掌握正确规律者和稀里糊涂者在面对问题时的不同。
用一个例子来展示“如何设计简单到明显没有错误”的软件吧——以office为例。
从哪开始呢?
——————————————————————————————————
100年前,甚至更早,人们已经认识到计算是可以机械化的了。于是,他们发明了算盘、计算尺、手摇计算机……甚至积分电路。
不过,以上种种都只能解决一个方面的简单问题;通用计算机?对不起,没门。世界是如此复杂……
然后,图灵大神诞生了。
——————————————————————————————————
图灵其实只是做了这么个工作:
1、用数字为操作编码,并把这些被编码的操作看做一台机器的基本零件
2、利用这些编码构建代码序列
3、输入代码序列,机器就按次序逐个执行代码序列中的编码对应的操作;而精心设计的、按次序执行的操作就可以完成某种功能(这是专用计算器,还不是通用计算机)
4、证明支持特定编码的机器,可以通过代码序列模拟任何其它机器——这种机器就被认为是图灵完备的,它就是通用计算机。
简单来说,图灵是说:实现一个根据指令代码序列做对应操作的机器,只要这个机器提供的操作指令之集合能达到“图灵完备”的程度,它就是通用计算机。
然后?基于这个“执行操作序列”的机制,请脑补如下策略:
1、把操作定义为数学运算指令和跳转指令(以后简称为指令)
2、用这些指令写程序吧
3、执行程序
4、证明1里面定义的操作是图灵完备的
这就是基于图灵机制的、以数学运算和跳转指令这个策略实现的通用计算机。
比如,x86就定义了一套指令;而arm则定义了另一套;power pc则又是一套……
它们都可以处理任何可计算问题。
所以:
1、图灵机是机制
2、具体的x86指令集是策略
3、x86指令集解决的,是“高效率支持图灵所预言的通用计算能力”问题
———————————————————————————————————
现在,我们开始设计高级语言——此时x86指令集是机制,我们设计的语言是一套策略,它将为常用软件的设计者提供一套机制:这套机制可以使得他们可以高效开发软件。
高级语言的示例之一就是C。它把x86/arm/power pc等等指令抽象为少量关键字/运算符,使得写程序像写英文、写数学公式一样简单。
———————————————————————————————————
终于到可以设计软件的时候了。我们写一套office软件吧……
嗯,这个真是太复杂了……
别急。
———————————————————————————————————
具体到我们这个需求上,c的能力太弱了。它提供的机制虽然足以让我们做任何事,但到office上,这就相当于要求我们要利用它的机制实现无限复杂的策略——而这是无法容忍的。
所以,第一步,先写一套glibc,封装基本的数据结构和算法吧——glibc是基于C提供的机制实现的一组算法/数据管理算法(策略)。
当然,对很多人来说,算法很难。不过,具体到某一个算法上,它并非不可理解。还是那句话,难者不会,会者不难。
好在,有glibc(以及其它很多库),写高层应用已经很少需要理睬那些基本算法了。
————————————————————————————————————
即使有了glibc,仍然不到写office的时候——虽然只靠C甚至机器指令也一定能做到——我们需要更强悍的机制。
基于glibc提供的机制(上一层次实现的一组策略,到下一层次就成了机制,这事实在太常见了),我们可以写个GTK。
GTK将抽象显示器上的一块块区域(窗口),并抽象用户(通过鼠标/键盘/其它输入设备)和它们的交互(消息)。
然后,窗口(以及消息)是机制,具体的某种widget是策略……
进一步的,抽象数据存储为model,抽象窗口为view,抽象动作逻辑为control——这就是MVC架构。
熟悉GUI的应该知道从微软的MFC架构到经典的MVC架构,GUI库的设计理念发生了多少改变——这里就可以看出,写“明显没有错误”的软件有多难了。
当然,有了GTK,基于GTK提供的机制,实现office就容易太多了。
而GTK本身虽然称得上庞大,但核心其实也就那么几个类以及简单的几个逻辑而已;其它五花八门的种种widget,其实不过是基于核心那几个类/算法提供的机制实现的一些策略罢了。
换言之,如果你深入分析GTK源码的话,轻易就会发现它内部其实也是分层的,每层实现若干策略,这些策略的作用是给更上层提供简单好用的机制……“上一层次实现的一组策略,到下一层次就成了机制”,这句话应该不会这么快就忘掉吧?
这种特点,决定了GTK可以成为高效、稳定的基础库。
—————————————————————————————————————
然后?开始写office吧?
别急,别急。首先我们需要定义一套文档格式,为了兼容,最好用xml格式;而xml本身需要一套库来存取(封装解析);此外,组成office的word/excel等应用需要相互调用,这需要某种通信机制甚至是OLE/COM(当然,这两个抽象很糟糕)……
总之,很明显,即便有了GTK等框架库,到office的路依旧漫长。我们仍然需要步步为营,作出一层层漂亮的抽象。但一旦这样做了,那么到了最后,就好像在程序中内嵌浏览器一样,简单到只有一步而已。
当然,这中间的过程,尚须从实践出发,一点点补足。但至少有一点可以预言:它必然是分层的,且提供的抽象必须尽可能基本,否则就会带来不必要的复杂度——比如从SDK到MFC到MVC,简化程度不啻于从地心说到日心说:注意地心说并非不能解释行星运动,只是相对于日心说,它太复杂了。
事实上,请自行研读著名开源项目的代码,甚至就是从微软的sdk里,都能随时随地发现以上设计原则的痕迹——对这个原则的应用水平是另一回事。
——————————————————————————————————
很显然,好的软件设计,依靠的不是什么TDD、UML、OO,而是发现规律、抽象规律、精简规律;只有到最后一步实现规律时,才可能用到OO之类提供的机制(而不是它的方法论)。
所谓简单到明显没有错误,其实就是:尽量让每一层都抽象出尽量少而基本、但同时还必须完备的规律(机制),让程序的正确性由规律来保证。这样,只要规律抽象没问题,程序就不会有问题——而规律是否正确,是非常容易考察的。
当然,每一层规律的具体抽象方式,可能都需要一个甚至多个地心说到日心说的演变过程才能走向完善。不合理的规律会带来基于这个规律的实现策略的复杂度(比如用地心说去描述行星运动)。这就是写“明显没有错误的软件”如此困难的根本原因。
至少在现在,这种抽象只能由人来完成,人工智能都无从染指。TDD之类机械的方法论要有用,把它写成一秒上亿步的人工智能多好?又快又不会出错,早就该替代设计师了。
——很明显,TDD/UML/OO之类的方法论根本就没资格在设计这个领域发言。虽然它们在具体实现某个东西时可能很犀利,但它们不是设计。它们甚至都提不出地心说,更不可能帮你从地心说优化到日心说。
拿它们来掺和软件设计,这不是错误,这是笑话。它的可行度,一点都不比“请一大群猴子随机敲键盘来输入莎士比亚全集”高。
当然,相比于那些完全不知规律为何物、只知道现实很复杂、复杂到超出他们掌控的人来说,TDD/UML/OO等方法论的确是一根不错的救命稻草……它至少使得一些人在装着做什么事时,能够更像神殿祭司而不是跳大神:这两者的唯一区别是前者有章法、后者随心所欲。但也就如此而已。
鞭辟入里!膜拜了!
发现“我国”程序员一个通病是动辄把问题上升到政治层面,人人都是政治家,历史学家,军事家。把个程序问题非要扯出点花儿来,所以,评论就总是打水战。
同意。反正都怪体制或者素质咯。
同意。你说有些连现代文明的基本原则都一无所知的家伙瞎过来搀合什么?只为了宣布他不会?
可那些TDD/UML/OO教徒,你不理他吧,他就得瑟上了。简单问题神秘化,然后?程序员一边去,你们是蓝领,让位,让白领的大祭司登场!
甚至,“写一个软件有两种思路,一种是简单到明显没有错误,一种是复杂到没有明显的错误。前者远比后者更难。”这句褒贬明确、毫无二义的话,都被他们断章取义,故意忽略最后一句话,把“复杂到没有明显的错误”当作一种可行的、严肃的设计方法来宣扬——不妨看看跟帖中有多少人现身说法吧。
战争总是从被攻击者的抵抗开始。正是这种本应去搞政治的所谓“程序员”的侵蚀,自然使得真正去搞技术的屡屡失去自己的净土,最后不得不被政治家拖到他们的水平上,然后再被他们打败。
我真的很难理解,某些人怎么能有那么厚的脸皮,居然能无视“写一个软件有两种思路,一种是简单到明显没有错误,一种是复杂到没有明显的错误。前者远比后者更难。”这句话里如此明显、辛辣的讽刺意味,掐掉尾句就敢拿它来给自己搞出的烂摊子辩护。这种恬不知耻,似乎是政治界的特长吧。
不妨拿个同样的句式作为对比:
治好一种疾病有两种方法,一种是找到病根并解决它,一种是杀掉病人。前者远比后者更难。
这种句式隐含的强烈褒贬、讽刺,是如此的鲜明、不容忽视。因为它(这种句式)设计来就是为了表达强烈的讥讽意味的。
可在这片神奇的土地上,“中国”的程序员却不得不出来辩护,以向大家证明“杀掉病人”,哦,不,是写出“复杂到没有明显错误”的程序,真的不应该是程序员的工作方式。
不得不承认,政治家们颠倒黑白的艺术,着实无敌。
大白告,闲逛到你地盘上,一猜就是你。
今天不碰巧,还去面了TW。无论敏捷开发也好,传统的瀑布也好,我觉得能抓到老鼠的猫就是好猫。
我可以不客气地告诉你, 你的软件工程要么是压根没学, 要不就是XXX吃了. 员工的快乐时间了在收益的基础上不是建立在劳动的基础上, 我想逗你快乐, 10倍工资挖墙角怎么都弄走了—-人本, 靠得住吗?
@invalid
说得太好了,测试是手段而不是目的,膜拜
“软件的质量不是测试出来的,而是设计和维护出来的。就像工匠们在一点一点地雕琢他们的作品一样。”
Bravo!
@TekTea
哥们你说得太对了,我之前遇到的一个公司,号称05年开始做敏捷开发。
他们搞的那个单元测试,纯粹就是为了提高测试覆盖率而测试,还硬性的加了一条测试覆盖率不达标,奖金取消,
然后他们突击写单元测试代码,很明显这样的测试没有任何的意义,而且还浪费了大家的时间和表情,实在BT和教条。
@时刻不得闲
你只说对了一部分,应该是对他们来说不重要的人,他们不在乎
想当年我的毕业设计就是测试驱动开发,但是几年工作下来,我一直对单元测试持矛盾的态度:单元测试当然是一种很好的创举,但是具体使用当中,它却经常会产生副作用。这不是因为单元测试的思想有问题,而是实践这一思想的人的问题。
当前很多公司使用单元测试的怪现象:
很多公司为了说出去好看——我们的开发遵循敏捷开发,拥有近100%的单元测试覆盖率,所以代码质量有非常可靠的保障。但是实际上,单元测试都是软件已经开发完成之后加上去的,而且经常还是由专门分配的几个人去写单元测试的,而这几个人根本不熟悉需求,甚至根本没有参与开发过程,或者参与得很少,而分配给他们写单元测试代码的时间当然也和紧张,因为这也是需要成本的。所以,最终导致的结果就是——为了达到覆盖率目标,为每个方法(不管有无必要)加上单元测试,简答看一下这个方法,然后从当前对此方法的肤浅理解出发,写一个或很少的几个测试用例,这样单元测试覆盖率100%的要求就达到了,可是这样的单元测试有用吗?答案不言自明。
My programmer is trying to convince me to move to .net from PHP. I have always disliked the idea because of the costs. But he’s tryiong none the less. I’ve been using Movable-type on various websites for about a year and am worried about switching to another platform. I have heard great things about blogengine.net. Is there a way I can transfer all my wordpress posts into it? Any help would be greatly appreciated!
事实上只有你才会扯什么“TDD追求100%覆盖率”这种鬼话。
这个口号就是连基本的测试都不懂的人,搞行政的人而不是复杂一点的软件的实践者,才会这样。
TDD是一种编程方法,将接口设计、行为设计跟实现代码一致起来(而很早以前写的纸质的设计文档可能在几个月以后代码实现时成为垃圾)。
TDD就是让你在写千分之一代码时,你先写写一下这千分之一代码的目标思路,而已。
只有那些根本就是搞手工测试的人,不懂自动化测试,才要追求什么100%测试覆盖率。实际上这样就能经得起验收测试么?不能。
而TDD目标是模拟这样的场景:自己如何驱动开发、项目经理如何能够验收,用户如何能够验收。
100%测试覆盖率,得到的仍然是一个垃圾。这谁都知道。只不过被那些根本不懂TDD的人拿来当作一个借口而已。
你用那些事后让根本不懂、不质疑、不参与软件设计的人低层次测试人员去写单元测试,然后就给TDD扣上帽子了。这怨谁呢?
一个人所付出的辛苦汗水,应该追着目的而付出,而不是为了底层次地为了走走形式地为了付出而付出。
在一些公司里,也许老板是“感觉”到应该学习别人的经验,但是手下的得力干将可能就是因为水平平庸、而因为其它原因才被选择作为得力干将的,那么这种人就只会把手底下的人带入低层次的开发泥潭里。这跟TDD无关。
TDD根本不是传统上那种事后才写的单元测试。仅凭这一点,就知道那种为了100%覆盖率而加上去的伪TDD代码根本不是TDD行为的产物。
Kent的回答让我感觉就像武功练到极致,开始无招胜有招了。
NARRATED ANIMATION OF PREVIEWING THE DOCKING
BRIEFING ON KIBO LABORATORY FACILITY
说得太精彩了!
那些抵毁面向对象这种思想的人和那些不懂面向对象思想而乱用、滥用的人其实一样是不懂面向对象的。
楼主英文真好啊, “ I do tend to make sense of test errors, so I’m extra careful when I have logic with complicated conditionals. When coding on a team, I modify my strategy to carefully test code that we, collectively, tend to get wrong.” 虽然每个单词都认识 但中文意思还是很模糊。楼主的翻译很精确。
“最亮的是这个问题是由Kent Beck,Kent是XP和TDD的创造者,是敏捷开发实践方法的奠基人。”这句话不通
估计应该是:“最亮的是这个答案是Kent Beck,Kent是XP和TDD的创造者,是敏捷开发实践方法的奠基人。”
对于那40亿单元测试的例子,解决办法是运用equivalence partitioning https://en.wikipedia.org/wiki/Equivalence_partitioning
用一个测试来代表所有同质(equivalent)的例子。
另外,对于如何选择效果最大化的测试,我们需要在代码中区分出算法(algorithm)和整合者(collaborator),集中测试算法,忽略整合者。详见https://yeminglakeforest.github.io/software/2018/04/25/to-test-or-not-to-test/