三问TDD: 单元测试总是好的吗?
原帖:再问TDD:
扩散角模型
有关测试“后行”也可以接受的说法,说明了一个事实:即使是最中坚的测试粉丝,也经常需要修正自我。很多理论抛出来之后,在现实面前,都不断的妥协。一些妥协到基本完善,一些妥协到基本完蛋。
说实话,我表面上是一个保守派,其实骨子里是个激进派。几年前我的想法深度和广度都不足,实际上也非常容易跟潮流; 现在情况稍微好了一点(我只敢这么说),所以我的激进反而表现在,无论你多牛的人提出,再多的人捧场,只要让我觉得不舒服,同时这种不舒服是存在道理支撑的,我就会比倡导者更早的进入砍掉多余内容或增加判定条件的过程。 如果我说我对流行趋势不屑一顾,那我太张狂了,也不是实情。实际情况是,我关注它们能带来的东西,但也确实持谨慎态度,同时或多或少的能感觉到一点它们必将发生的修正道路。
关于先行还是后行(虽然后行已然很难说是TDD,同时失去了先行测试的一些重要优势),其实我在乎的不是这个。存在很多情况,后行会比较舒服,这是必然的。但是关于“必须”这个词,我觉得还是太绝对了。我们都不必考虑单元测试对静态语言到底发挥多大作用,也不必去注意单元测试的最大受益者是动态语言这一事实,只要再考虑一下我说的“扩散角”模型就够了。
在很多情况下,一个隐含BUG但能以95%的概率正确完成任务的东西,比因为抱有单元测试在各个粒度级别必须达到一定覆盖的想法,而超过了成本能承担的极限所造成的问题,还是要好得多。 反过来,另一些情况下,一个隐含着定时炸弹的程序是相当危险的,比如医疗方面的嵌入式软件。 虽然软件和参与人员的规模本身就会对流程和组织结构的进行产生重大影响,但是如上所述的基本道理,仍然是在何种程度上确立测试的标准和规模的重要因素。
如果让我倡导测试理念,我觉得除了掌握好测试的切入点和目标的粒度,一个首先要学会的东西,就是在列表里划出优先级,同时标出不可获缺的测试,和那些如果砍掉对置信度影响极其轻微的测试。 另外一方面,排除那些“先行测试”的重大优势,具体到可以“后行”的单元测试,我认为单元测试的必要性,很多时候来自于开发者脑中对该单元的不确定性。有些时候,直接抹掉不确定性的成本要高于使用测试的成本,这时候测试就是必要的;而再另一些时候,测试不过是对脑海中确定的东西换一种方式重复一遍罢了。
后者本身是不需要什么单元测试的,但是很多人总在强调一件事:是人就会犯错。但这种说法没有考虑该事实的另外一个方面:既然是人就会犯错,当你重复自己的时候,或别人重复相同的知识的时候,也仍然存在犯错的几率。这个事实所带来的结果可能是非常让人惊诧的:当我们无差别的认为所有目标都应该进行单元测试时,有可能使得结果的置信度被降低; 最终要么造成更大的麻烦,要么反而使得软件的可靠性受到影响。 这或许有点耸人听闻,但这就是我为什么强调,增加一个环节,有时候并不只是增加工作量,而是引入更多的混乱的其中一个可能的原因。
所以在这里,我可以很个人的给单元测试增加一个“必要性指标”,这个指标是由测试目标可能存在的不确定性所决定的。对于较大型的组织来说,应该对这事有一个统一的判断标准,哪怕仅仅是测试目标的预期的代码行数; 更好的做法是对复杂度建立起统一且规范的判定标准。这样做甚至对于那些肯定应该进行的测试,也能起到一定的辅助作用(比如认识上的)。
好钢要使在刀刃上。说到底,无论是测试,还是硬生生的调试,我们关心的不是别的,还是成本的问题: 哪怕一团糟,我们如果有无限的时间,无限的金钱,爱怎么写根本无所谓。所以当我们说测试是“必须”的时候,或者说测试毫无用处的时候,最好还是先看看表,数数人头,再摸摸自己的钱包; 也许他们会让你不得不选择去写某些测试,但是对另一些情况和其它一些测试,答案正好相反。另外,如果对负面效果无法做出清晰的认识,我们可以去用,但不要对测试产生依赖性。
总而言之,很多测试倡导者的问题,恰恰不是过于重视测试,而是对待测试的态度过于轻易了。我们无论使用什么手段,实质上追求的仅仅是一个概率,这数字到底是会上升还是下降,是一个问题; 提高每一个百分比所要付出的代价,也还是要精打细算才成。 对每一个可能带来变化的决定,只有四个字: 如临大敌,如履薄冰。
另外,我希望大家注意一个倡导了20年的,比测试要轻量的多,甚至更应该作为一种良好习惯的编程技巧,既子程序(Routine)内的断言。一个测试如果“后行”是可以被允许的,那么也许这个测试可以部分的用断言代替:当断言认证了所有造成不确定的点,我们就没必要在测试中重复这些知识,从而避免引入新的不确定性。 凡是适合使用断言增加置信度的地方,我个人的看法是,断言的性价比相对测试要高得多;唯一需要注意的是,断言在某些场景下也会连带一些负面的效果,或者成本比测试还要高; 这仍然是对不同手段适用性的判断。更多的思考是,在稍微大一些的组织内,我们无法确认程序员是不是正确的在子程序内使用了断言。但在这上面,我觉得,信任程序员比信任测试员好: 对相同的知识,前者有一个产生风险的主体,后者却有两个。
说实话,我觉得有些大牛和他们的文章都有点,怎么说呢? 不够严肃,或者说不够严谨。 实际上建立简单的模型和进行基本的分析,对一些要素的覆盖达到一定比例,就可以获得更好的指导原则;但是我要说的是,他们在宣传时忽略了这些工作,或者说对这些方面没有很好的展开;更没有尽职尽责的把这些表面之下的工作结果,推广到受众群里去。一些人可能是没时间,也没这个义务;但另一些人,就像我说MS隐藏ASP.NET的复杂性一样,是故意的。
我觉得这才是我这类哗众取宠之人存在的最大的必要性:做一个提醒。但是我这类人也绝不可能花几个月时间,进行分析研究工作,然后拿出一个严谨的,令人信服的行动的初步指南。这些工作还是必须大家自己去做,尤其是某一理念的追随者们需要付出更多。
最后说明,如果场景要求或者规定非测试不可,我更多的倾向于“先行测试”;主要是因为“先行测试”不仅仅带来提高置信度的好处: 在很多情况下,这些额外的东西才是TDD的理念或者比如使用测试的方法如敏捷所关心的首要问题。另外,我不太想探讨测试或具体某种测试的必要性;我真正想强调的是,我们需要更多的拥有足够判断力的人,才可以做出更多的正确的决定,让从行业到具体组织内部的软件构件水平,上升一个台阶。
Update:
附录:
取自Venus神庙的小扯一下调试。。。后半段。红色为怪怪的注释。
到调试器,不得不说测试(这里指的是程序员个人的单元测试...)。在此前一点概念需要说明。在很多人概念里,调试简单的对应使用调试器,与测试是格格 不入的。而在本书中,调试,指为了发现错误的起因所使用的所有手段,其中包含测试。测试最最最最优良的一点在于它是自动化的(这是个要点)。或者说是可永远重现的(这是付出好几方面的代价交换来的,因为测试也是人手工编写的)。这就 是说,使用测试,可以帮你解决诱发错误,重现错误等及其麻烦的事情。但是,测试往往只是能够帮你找到错误的出现点,而不总是能快速的把你带到错误的起因 点,而为了部署这些测试你需要花费很多的精力。正是基于此,包括云风老大(此人真是让人又爱又恨...,从这方面来说和linus有点像)等很多人不屑使用测试(再次强调,这是指个人的单元测试...)。但是,这部以为这测试不重要,而成为你可以肆意偷懒放低代码质量的借口。而是因为其带来的好处被其他的一些技术抵消了(也可以被非技术因素抵消),这样的话,为其付出的代价就会显得很沉重。
但是无论你使用什么样的手段进行调试。代码结构的质量才是最最最根本的内容。一个函数划分很细致,不大量滥用全局变量的代码,使用很多无副作用的函数,通 过测试你发现错误出现点,往往你就能很快到达错误起点(因为你把错误可能出现的地方限定在了有限的范围)。而如果你代码结构混沌不堪,函数/类之间关系错 综复杂,哪怕你用测试找到了错误出现点,开着调试器进去看,你也会很快转晕。所以提高代码质量,合理应用适合的调试工具和手段,正确使用调试的方法才是正 道。在这里,不得不提一下TDD。最初的时候我总觉得TDD最核心的是T,Test。后来才开始明白,它最核心的其实是D,Drive(我非常认同,这就是我说的测试之外的东西)。你可以把测试写的 很弱,但你一定要在此影响下把代码重构的很好。由此得到一个蛮歪的理:如果你或你团队的代码素质很高,可以尝试不用TDD的一些开发手段(不认同简单的以人的水平决定,而是如我正文所说,按照一定的条件作出判断);但如果你或你团 队代码素质不够高,请把自己套在TDD里面磨练一下。之所以说歪,是因为我其实只想说后半句,前半句纯属为了对仗工整而用。
最后我发现我忘提一个很好玩的东西,断言。我一直觉得这个东西很有意思。是一种游离在单元测试与调试器之外的手段,是一个隐藏在事后分析中的事前推测派来 的间谍。assert的意义在于猜测并提醒一些最有可能的错误,它不精确但也不死板。我一直觉得,全面合理的铺下断言这张网(三个部分,输入输出不变 式),可以很有效的提高调试速度。(呵呵,对这段没啥可说的,支持)
有关测试“后行”也可以接受的说法,说明了一个事实:即使是最中坚的测试粉丝,也经常需要修正自我。很多理论抛出来之后,在现实面前,都不断的妥协。一些妥协到基本完善,一些妥协到基本完蛋。
说实话,我表面上是一个保守派,其实骨子里是个激进派。几年前我的想法深度和广度都不足,实际上也非常容易跟潮流; 现在情况稍微好了一点(我只敢这么说),所以我的激进反而表现在,无论你多牛的人提出,再多的人捧场,只要让我觉得不舒服,同时这种不舒服是存在道理支撑的,我就会比倡导者更早的进入砍掉多余内容或增加判定条件的过程。 如果我说我对流行趋势不屑一顾,那我太张狂了,也不是实情。实际情况是,我关注它们能带来的东西,但也确实持谨慎态度,同时或多或少的能感觉到一点它们必将发生的修正道路。
关于先行还是后行(虽然后行已然很难说是TDD,同时失去了先行测试的一些重要优势),其实我在乎的不是这个。存在很多情况,后行会比较舒服,这是必然的。但是关于“必须”这个词,我觉得还是太绝对了。我们都不必考虑单元测试对静态语言到底发挥多大作用,也不必去注意单元测试的最大受益者是动态语言这一事实,只要再考虑一下我说的“扩散角”模型就够了。
在很多情况下,一个隐含BUG但能以95%的概率正确完成任务的东西,比因为抱有单元测试在各个粒度级别必须达到一定覆盖的想法,而超过了成本能承担的极限所造成的问题,还是要好得多。 反过来,另一些情况下,一个隐含着定时炸弹的程序是相当危险的,比如医疗方面的嵌入式软件。 虽然软件和参与人员的规模本身就会对流程和组织结构的进行产生重大影响,但是如上所述的基本道理,仍然是在何种程度上确立测试的标准和规模的重要因素。
如果让我倡导测试理念,我觉得除了掌握好测试的切入点和目标的粒度,一个首先要学会的东西,就是在列表里划出优先级,同时标出不可获缺的测试,和那些如果砍掉对置信度影响极其轻微的测试。 另外一方面,排除那些“先行测试”的重大优势,具体到可以“后行”的单元测试,我认为单元测试的必要性,很多时候来自于开发者脑中对该单元的不确定性。有些时候,直接抹掉不确定性的成本要高于使用测试的成本,这时候测试就是必要的;而再另一些时候,测试不过是对脑海中确定的东西换一种方式重复一遍罢了。
后者本身是不需要什么单元测试的,但是很多人总在强调一件事:是人就会犯错。但这种说法没有考虑该事实的另外一个方面:既然是人就会犯错,当你重复自己的时候,或别人重复相同的知识的时候,也仍然存在犯错的几率。这个事实所带来的结果可能是非常让人惊诧的:当我们无差别的认为所有目标都应该进行单元测试时,有可能使得结果的置信度被降低; 最终要么造成更大的麻烦,要么反而使得软件的可靠性受到影响。 这或许有点耸人听闻,但这就是我为什么强调,增加一个环节,有时候并不只是增加工作量,而是引入更多的混乱的其中一个可能的原因。
所以在这里,我可以很个人的给单元测试增加一个“必要性指标”,这个指标是由测试目标可能存在的不确定性所决定的。对于较大型的组织来说,应该对这事有一个统一的判断标准,哪怕仅仅是测试目标的预期的代码行数; 更好的做法是对复杂度建立起统一且规范的判定标准。这样做甚至对于那些肯定应该进行的测试,也能起到一定的辅助作用(比如认识上的)。
好钢要使在刀刃上。说到底,无论是测试,还是硬生生的调试,我们关心的不是别的,还是成本的问题: 哪怕一团糟,我们如果有无限的时间,无限的金钱,爱怎么写根本无所谓。所以当我们说测试是“必须”的时候,或者说测试毫无用处的时候,最好还是先看看表,数数人头,再摸摸自己的钱包; 也许他们会让你不得不选择去写某些测试,但是对另一些情况和其它一些测试,答案正好相反。另外,如果对负面效果无法做出清晰的认识,我们可以去用,但不要对测试产生依赖性。
总而言之,很多测试倡导者的问题,恰恰不是过于重视测试,而是对待测试的态度过于轻易了。我们无论使用什么手段,实质上追求的仅仅是一个概率,这数字到底是会上升还是下降,是一个问题; 提高每一个百分比所要付出的代价,也还是要精打细算才成。 对每一个可能带来变化的决定,只有四个字: 如临大敌,如履薄冰。
另外,我希望大家注意一个倡导了20年的,比测试要轻量的多,甚至更应该作为一种良好习惯的编程技巧,既子程序(Routine)内的断言。一个测试如果“后行”是可以被允许的,那么也许这个测试可以部分的用断言代替:当断言认证了所有造成不确定的点,我们就没必要在测试中重复这些知识,从而避免引入新的不确定性。 凡是适合使用断言增加置信度的地方,我个人的看法是,断言的性价比相对测试要高得多;唯一需要注意的是,断言在某些场景下也会连带一些负面的效果,或者成本比测试还要高; 这仍然是对不同手段适用性的判断。更多的思考是,在稍微大一些的组织内,我们无法确认程序员是不是正确的在子程序内使用了断言。但在这上面,我觉得,信任程序员比信任测试员好: 对相同的知识,前者有一个产生风险的主体,后者却有两个。
说实话,我觉得有些大牛和他们的文章都有点,怎么说呢? 不够严肃,或者说不够严谨。 实际上建立简单的模型和进行基本的分析,对一些要素的覆盖达到一定比例,就可以获得更好的指导原则;但是我要说的是,他们在宣传时忽略了这些工作,或者说对这些方面没有很好的展开;更没有尽职尽责的把这些表面之下的工作结果,推广到受众群里去。一些人可能是没时间,也没这个义务;但另一些人,就像我说MS隐藏ASP.NET的复杂性一样,是故意的。
我觉得这才是我这类哗众取宠之人存在的最大的必要性:做一个提醒。但是我这类人也绝不可能花几个月时间,进行分析研究工作,然后拿出一个严谨的,令人信服的行动的初步指南。这些工作还是必须大家自己去做,尤其是某一理念的追随者们需要付出更多。
最后说明,如果场景要求或者规定非测试不可,我更多的倾向于“先行测试”;主要是因为“先行测试”不仅仅带来提高置信度的好处: 在很多情况下,这些额外的东西才是TDD的理念或者比如使用测试的方法如敏捷所关心的首要问题。另外,我不太想探讨测试或具体某种测试的必要性;我真正想强调的是,我们需要更多的拥有足够判断力的人,才可以做出更多的正确的决定,让从行业到具体组织内部的软件构件水平,上升一个台阶。
Update:
附录:
取自Venus神庙的小扯一下调试。。。后半段。红色为怪怪的注释。
到调试器,不得不说测试(这里指的是程序员个人的单元测试...)。在此前一点概念需要说明。在很多人概念里,调试简单的对应使用调试器,与测试是格格 不入的。而在本书中,调试,指为了发现错误的起因所使用的所有手段,其中包含测试。测试最最最最优良的一点在于它是自动化的(这是个要点)。或者说是可永远重现的(这是付出好几方面的代价交换来的,因为测试也是人手工编写的)。这就 是说,使用测试,可以帮你解决诱发错误,重现错误等及其麻烦的事情。但是,测试往往只是能够帮你找到错误的出现点,而不总是能快速的把你带到错误的起因 点,而为了部署这些测试你需要花费很多的精力。正是基于此,包括云风老大(此人真是让人又爱又恨...,从这方面来说和linus有点像)等很多人不屑使用测试(再次强调,这是指个人的单元测试...)。但是,这部以为这测试不重要,而成为你可以肆意偷懒放低代码质量的借口。而是因为其带来的好处被其他的一些技术抵消了(也可以被非技术因素抵消),这样的话,为其付出的代价就会显得很沉重。
但是无论你使用什么样的手段进行调试。代码结构的质量才是最最最根本的内容。一个函数划分很细致,不大量滥用全局变量的代码,使用很多无副作用的函数,通 过测试你发现错误出现点,往往你就能很快到达错误起点(因为你把错误可能出现的地方限定在了有限的范围)。而如果你代码结构混沌不堪,函数/类之间关系错 综复杂,哪怕你用测试找到了错误出现点,开着调试器进去看,你也会很快转晕。所以提高代码质量,合理应用适合的调试工具和手段,正确使用调试的方法才是正 道。在这里,不得不提一下TDD。最初的时候我总觉得TDD最核心的是T,Test。后来才开始明白,它最核心的其实是D,Drive(我非常认同,这就是我说的测试之外的东西)。你可以把测试写的 很弱,但你一定要在此影响下把代码重构的很好。由此得到一个蛮歪的理:如果你或你团队的代码素质很高,可以尝试不用TDD的一些开发手段(不认同简单的以人的水平决定,而是如我正文所说,按照一定的条件作出判断);但如果你或你团 队代码素质不够高,请把自己套在TDD里面磨练一下。之所以说歪,是因为我其实只想说后半句,前半句纯属为了对仗工整而用。
最后我发现我忘提一个很好玩的东西,断言。我一直觉得这个东西很有意思。是一种游离在单元测试与调试器之外的手段,是一个隐藏在事后分析中的事前推测派来 的间谍。assert的意义在于猜测并提醒一些最有可能的错误,它不精确但也不死板。我一直觉得,全面合理的铺下断言这张网(三个部分,输入输出不变 式),可以很有效的提高调试速度。(呵呵,对这段没啥可说的,支持)