找回密码
 立即注册

QQ登录

只需一步,快速开始

graper

高级会员

45

主题

63

帖子

1348

积分

高级会员

积分
1348

活字格认证

graper
高级会员   /  发表于:2009-12-11 15:50  /   查看:6013  /  回复:0
Post by "MultiRow",  08-26-2009, 1:07
-----------------------------------------------------


今天看到@jeffz_cn在twitter上问:“私有方法真的不应该单元测试吗?为什么?我觉得有的组件只是逻辑复杂一些,因此会提取私有方法,并且测试这些私有方法的逻辑。如果把这些内容统统从外部“注入”,这样私有的逻辑就变公开了……但是这样难道没有过渡设计的味道吗?”。
然后就想起来我在项目中推动单元测试的经过。觉得还是应该总结一下比较好。

先说现状 (下面的数据我现在无法核实,但是,应该和实际值误差不大)
我目前负责的项目,有代码200K+,控件产品,尤其是Grid控件产品的代码复杂度远比应用程序的产品复杂度高。因为功能级的耦合度就很高。因此,我认为我的产品的复杂度应该相当于普通应用程序500K+的水平。
目前单元测试有1300+。这些单元测试主要是自5.1和6.0阶段引入的。对遗留代码的单元测试很少。这两个阶段添加和修改的代码应该在130K+。(呵呵,看到这里你一定觉得数据有问题。确实看起来有问题。但是,细节这里就不能多说了。)
目前的单元测试代码覆盖率应该在20%~25%之间。
目前单元测试集成在每日构建中。至今没有发现单元测试失败的情况。(这一点很费解,目前归结为狗屎运)

再说经验
1. 单元测试应该在物理设计阶段进行规划,而不是完成代码后补单元测试。
2. Mock类库一般情况下是鸡肋
3. 对已有代码编写单元测试的难度非常高
4. 当单元测试很多的时候,组织和命名会比较有挑战。
5. 目前很少遇到单元测试影响重构的情况。
6. 单元测试对重构的帮助不如预期
7. 目前的现状下,很多平台的限制,使能够单元测试的部分很少。

再说想法
1. 单元测试可以作为开发Leader掌控设计的一种工具
2. 单元测试可以帮助开发人员设计出更好的结构
3. 单元测试不需要对private成员进行。

-=-=-=-=-=-=-=-=-=-=-=-不明真相围观的分割线-=-=-=-=-=-=-=-=-=-=-=-=-=-
好,接下来在一个一个展开来说。

1. 单元测试应该在物理设计阶段进行规划,而不是完成代码后。
实践告诉我,单元测试是需要良好的设计来支撑的。一个耦合度很高的模块几乎没有办法进行单元测试。我曾经几次相对已有的代码进行一些重构来支持单元测试。最终都放弃了。因为对这些耦合度很好的模块的重构总是会引入一些不可预期的问题。最终投入都要远远超过我的预计。因此,我得出的经验是:单元测试需要在物理设计时期就思考所涉及模块的可测试性,为了可测试性,需要对设计进行一些调整。往往这种调整都会使设计更好。因为,耦合性和可测试性是成反比的,因此可测试性越高,也就证明耦合性越低。低耦合是目前大家已经公认的良好设计的标准。

2. Mock类库一般情况下都是鸡肋
我在开始推动单元测试的时候就详细的研究了Rhino.Mocks类库。当时也被它强大语法能力所折服。并且实际将该类库应用在了我们项目的单元测试中。可是,过了一段时间后,当我再次需要使用Mock对象的时候。我才发现,我自己写一个Mock对象的成本其实非常低。远低于学习Rhino.Mocks抽象的语法的成本低。因此,我建议你除非能够确认你每天(至少每周)都要用到Mock对象。否则,建议不要使用Mock类库。
因为,Mock类库的接口设计往往和我们开发人员(尤其是静态类型语言开发人员)的思维方式不一致。一段时间不用这些类库的时候,你就会忘记他们抽象的语法。就需要再付出时间去学习他们的语法。但是,对于一些特定的测试场景,编写简单的Mock对象的成本本身就非常低的。往往5分钟就可以写出来自己用着很爽的Mock对象。
但是,不推荐使用Mock类库,不等于你不需要学习和了解Mock类库。因为学习他们的接口会对你自己设计Mock对象非常有帮助。

3. 对已有代码编写单元测试的难度非常大
因为我们做的是控件产品,在兼容性方面的要求很多时候会很苛刻。有时候一个产品发布之后,发现了Bug,下一个版本也要保证这个Bug原封不动的表现在那里(这时候,我们会说这是我们产品的一个Design)。因此,对代码的重构就会成本很高。因此,要想在不破坏原有结果的情况下进行单元测试的难度就非常大了。这一点,也许有我们产品的特殊性所在。但是,我觉的目前现实中的很多项目其实和我们的项目的要求还是很像吧。

4. 当单元测试很多的时候,组织和命名会比较有挑战。
我一直没有建立起来一套好的单元测试命名体系。目前在项目中的组织方式是:两个平行的工程,产品工程使用InternalVisibleToAttribute为测试工程提供Internal成员的访问权限。两个工程保持相似的组织方式。但是,当一个被测类型很庞大的时候,测试代码就很难组织好了。

5. 目前很少遇到单元测试影响重构的情况
不好的单元测试或过度测试都会对重构带来不好的影响,在我参与的上一个项目中就出现过这种情况。当时,项目突击了一段时间的单元测试。硬任务,每人必须写nnn个单元测试。后来,产品升级的过程中,就不断的删除原有的单元测试。那是因为后来没有人将单元测试作为指标了。否则,可能很多有价值的重构都会不做了。因为修改单元测试太费劲了。这一点对我造成了阴影。以至于我在当前项目的前期没有很高调的推动单元测试。这是这个项目过程中我最大的遗憾之一。
也许是因为,单元测试覆盖率较低;也许是因为我们没有拿单元测试再作为指标,因此大家写的单元测试的质量更高了。总之在当前这个项目的升级中,似乎很少发现维护单元测试付出较大的情况。

6. 单元测试对重构的帮助不如预期
正如前面所说,我目前负责的项目中,较大的重构发生次数并不多。小规模的重构中确实有单元测试帮助我发现问题的情况。但是,远不像我的预期那么多。这一点,应该说和单元测试的覆盖率较低有关。

7. 目前的现状下,很多平台的限制,使能够单元测试的部分很少。
虽然我很有意识的推动单元测试,并且在实际开发中使用单元测试。但是,目前的情况,在WinForm平台下的开发中进行单元测试的桎梏还是很多。也许和我们的产品特性有关,实际过程中,我经常发现,能够测试的代码不是那些经常出问题的代码。而经常出问题的代码,往往因为和平台关系太密切,而无法切割出来进行单元测试。
ASP.NET MVC在一开始设计的时候就考虑了可测试性,因此,这一方面应该更好一些。但是,至少我目前没有看到微软在其他平台下的可单元测试方面的努力。这是我在使用单元测试过程中最郁闷的地方。

-=-=-=-=-=-=-=-=-=-=-=-不明真相围观的分割线-=-=-=-=-=-=-=-=-=-=-=-=-=-
以下最后两点属于我的想法,目前的项目中因为前面的一些约束,导致我还无法证明我的想法是正确的或错误的。列在这里也仅供大家参考了。

8. 单元测试可以作为开发Leader掌控设计的一种工具
和聪明的人在一起工作的最大困难就是你没有办法控制项目的设计。聪明人并不一定出好的设计。但是,你又没有办法说服他采用你的设计。我觉得,单元测试是一个开发Leader掌控设计质量的很好的工具。因为它可以成为一个简单的指标:“你别给我说你的设计有多么好,如果你的设计不可测试,那么抱歉,你不能放入产品代码。”,反过来说,如果你的设计可测试,那么意味着,即使你的设计再烂,它也是可以替换的。总有一天,我会把它从产品里面干掉的。
说说而已,其实大多时候,我也不确定我的设计是好的。但是,我相信,可测试≈低耦合≈好的设计。我相信,当项目复杂到一定程度的时候,建立一些这样简单粗暴可测量的规矩,对产品的健康发展很有帮助。

9. 单元测试可以帮助开发人员设计出更好的结构
因为那个简单粗暴可测量的规矩,迫使开发人员降低自己设计的耦合度。从而产生更好的设计。

-=-=-=-=-=-=-=-=-=-=-=-不明真相围观的分割线-=-=-=-=-=-=-=-=-=-=-=-=-=-
最后,来和老赵探讨一下他的问题。我的观点是:
10. 单元测试不需要对private成员进行。如果需要,那么抽象Strategy类。并对Strategy类进行测试。这个不属于过度设计。
因为,我认为需要测试的方法一定具有以下几个特点中的至少一个:

    * 它有出错的可能。
    * 它具有的复用的可能。
    * 它具有变化的可能。

对于第一点,我认为应该是可以通过对public成员的测试来完成对该private方法的测试的。而二三两点,正是抽象的用武之地。抽象的重要目的就是在封装变化和复用。如果这个函数具有了变化和复用的可能性,我们就应该将它抽象为一个独立的对象,并且对他进行测试。这是一个更好的设计,而不应该归入过度设计的范畴。
如果不符合上面的二三两点,我觉的对这个private成员的测试就属于过度测试的范畴了。是应该杜绝的。因为,你的测试代码很可能没有起到保证质量的作用,而是成为了将来重构的桎梏。因为,理论上重构的过程不需要保持私有成员的行为不变。但是,你的单元测试又要求私有成员的行为不变。这个其实就是我上文中提到的“单元测试影响了重构进行”的情况。

-------------------------------------
本文采用CC2.5署名非商业许可协议发布。原载于Colin的技术。转载请注明出处。细节请参考该网站右侧栏中的版权声明。


Reply by "WantSong", 09-04-2009, 0:08
-----------------------------------------------------

我说说我的一些看法。

“控件产品,尤其是Grid控件产品的代码复杂度远比应用程序的产品复杂度高。因为功能级的耦合度就很高。因此,我认为我的产品的复杂度应该相当于普通应用程序500K+的水平。”
我有三个疑问,
1,控件产品比应用程序复杂?
按照功能划分,软件有三类,系统软件,支撑软件和应用软件。控件无疑属于支撑软件——“协助用户开发软件的工具软件”,楼主正在负责一款支撑软件的开发工作。而楼主描述的“应用程序”显然属于应用软件。我们看看应用软件/程序包括什么。
如果按照软件服务对象划分,一个是项目软件,一个是产品软件。项目软件,如神六的监测系统,各种形形色色行业定制软件。产品软件,如数据库,Office,还有偶喜欢的CS。
我始终不能理解,这个“比……复杂”凭何而来。
2.复杂度是如何被度量的?
软件复杂性度量的参数很多,主要有:规模(总共的指令数或源代码行数),难度(程序中出现的操作数的数目所决定的量),结构(程序结构有关的度量)和智能度(算法的难易程度)。
支撑软件与应用软件的所涉及的领域是很不相同的,只是提示下在你是否将“普通应用程序”的业务规则(难度)和结构(业务建模)这两大块因素考虑在内。
3,代码规模与复杂度有必然联系么?
我看不出来, 200K+与500K+两个“普通”应用软件的复杂度的必然差距。

“单元测试有1300+,添加和修改的代码应该在130K+”,你也说有问题了,就说覆盖率,“目前的单元测试代码覆盖率应该在20%~25%之间”。
这个用NCover跑一下就知道覆盖率结果了,很容易。


“单元测试应该在物理设计阶段进行规划,而不是完成代码后”
很赞同你把单元测试方面的考虑提前。
你谈到规划。我这样来假设,你的项目生命周期是概念设计、逻辑设计和物理设计,接下来是实现和测试。概念设计主要是收集需求,分析需求。逻辑设计会定义系统的组成部分,各部分结构。物理设计,则主要是类和方法的设计。
不知道你们有没有架构设计阶段,如果有,应该在架构设计阶段完成测试的框架结构和约束。架构中应该考虑所采用的测试手段,对测试手段的支撑(在哪里封装)和约束(哪些是必须测试的,哪些不用)。尤其当你的系统与外部系统(如文件系统、数据库系统、Web Service系统等等)有联系时,你要提前考虑如何测试。这个架构设计应该在物理设计开始前结束。
单元测试是质量保证的手段,在向前,应该在质量规划阶段,定义单元测试的各项指标,如测试密度和测试覆盖率。


“Mock类库一般情况下都是鸡肋”
首先要区分测试手段和某个测试手段所采用的工具。
是为了封装“难以控制或无法控制的部分”,以便测试,动态方式是Mock,静态方式是Stub。这个部分,横着看,可能是层,或者是外部系统;竖着看,可能是模块,也可能是子系统。如果采用动态方式,能使用的工具无外乎是Service Locator或IOC/AOP。Mock类库大多采用了后者。
自己写的成本,你可能会用Service Locator,或者反射或者动态代理中的一种,我不认为Remoting方式的成本就几行。但是否使用,在哪里使用,如何使用,这些都应该是架构设计(或者在开发前阶段)的约束的一部分。
每个人在Mock时所使用的方式都不一样,这样的代码应该属于研究性项目。


“Mock类库的接口设计往往和我们开发人员(尤其是静态类型语言开发人员)的思维方式不一致”
严重的不敢苟同。据我所知,Rhino.Mock和Nunit.Mock都具有良好的OO设计。“静态类型语言”,4.0以前Microsoft引入动态语言的概念了么?
这使我产生了一个好奇,你们的代码中会有很多类,这些类中有很多方法,这些方法是按照什么规则放在这个类上,而不是其他的类。
我的看法,如果没有OOA,就没有OOD,没有OOD,谈何OOP。


“对已有代码编写单元测试的难度非常大”
非常不赞同这个观点。因为它没有限制条件,
在没有条件TDD时,单元测试往往是对“已有代码”编写的。实质性问题不是代码是否已经存在,而是你的设计不支持你去做单元测试。


“当单元测试很多的时候,组织和命名会比较有挑战。”
组织,放在一个工程中,还是放在两个工程中,是个见仁见智的问题。
“但是,当一个被测类型很庞大的时候,测试代码就很难组织好了。”这个依赖于你在架构时对测试的规划,如果你对测试的范围定义很清晰,这个问题会迎刃而解。
命名也的确是个问题。
我们后期的做法是,测试方法名为:所测试的方法名+Test+序号。在测试方法的注释中描述清楚测试目的、手段、环境、填入值、预期值等等一切你希望看到的,当然这个注释规范也是架构约束的一部分。
这样,你可以查看根据代码注释生成的CHM,来检查单元测试用例情况。


“目前很少遇到单元测试影响重构的情况”
这句话说反了吧,重构完全依赖于单元测试,以我的经验,它也的确是。如果你是重写的话,连接口都推翻了,那倒的确是一种影响。
如果说有严重影响的,那就是变更。它和单元测试相互作用,但影响的是成本,而不是质量。


关于单元测试的两个作用,帮助“开发Leader掌控设计”和“帮助开发人员设计出更好的结构”。
据我所知,你们并没有敏捷,单元测试的作用应该只会局限于质量层面,比如迫使开发人员不但要完成代码,而且要对完成的代码质量负责。
我并没有看到单元测试在设计上的可借鉴的作用,我只看到了你们在设计上的无奈。

“单元测试不需要对private成员进行”
类似的,这个应该是架构解决的问题,约束中可以定义“不需要对private方法进行”,但或许同样需要定义出这个约束的例外,比如某个类的私有方法只会被反射调用的,或者这个私有成员将会受到其他系统的驱动,与Mock相比,测试私有方法的成本可能更低。


我想楼主看到我的这些个逐条批注时,可能会蹦起来。:)
从05年尝试着用单元测试到现在,感觉在项目中推广单元测试,遇到的最大阻力是成本,因此单元测试的范围定义很重要,对于吃力不讨好,完全可以不用单元测试这种方式。
其次,是质量意识,也就是来自于开发人员的阻力。不知道“1300+”是Case数,还是代码行数,如果是前者,1300个Test Case覆盖了30K的代码,测试密度达到43 Cases/K Lines,可以认为是一个正常水平。
但若是后者,我只能说,你还有很多隐情没有告诉我们。即使覆盖了这“碰过的”30K代码,你不能保证你没碰过的那些代码不受任何影响。你要证明,不受影响,就需要提高覆盖率。而这,就是它为什么是重构的保证。



Reply by "KevinShan", 09-04-2009, 9:23
-----------------------------------------------------

楼上回复的非常精彩,顶一个!

不过就个人观点而言,楼主是实际经验总结,根据项目特点和各种外在约束在做单元测试。是实用主义的体现。

楼上比较注重理论,有学院派风范。



我比较赞同楼主的做法,在并非TDD做的老项目中,很可能有的地方连OOD的要求都达不到或者做的不好的情况下,以有限的资源逐步提高单元测试的覆盖率,是投入产出比很好的做法。只要不停完善,就能达到楼上的要求了。


Reply by "MultiRow", 09-08-2009, 21:19
-----------------------------------------------------



好吧,既然WantSong认为我必然要跳起来,那我就跳起来吧。 我喜欢你的PK风格,那我也针锋相对了. 并且,我希望更多的人参与进来,碰撞才能出火花嘛。

>> 1,控件产品比应用程序复杂?

可能我没有说清楚,严格来说“控件产品的开发在代码结构方面的复杂度要比一般面向特定业务定制的应用程序复杂”,我在文中也是使用“200K代码相当于500K”这样的比较,我想我应该将我的意思表达的比较清楚了吧。

大多时候,应用程序的开发在前期设计阶段可以对项目的结构进行很好的拆分。从而极大的降低程序各部分的耦合度。但是,控件开发中,为了照顾用户的易用性,为了性能的考虑,可以说最后的设计是几乎所有的功能都会和所有的功能产生联系,并且其中会暴露很多为开发人员预留的定制接口。这些都很现实的增加了内部结构的复杂度。就像一个5米长的绳子平放在地上和2米长的线揉成团,哪个更复杂?

我并不想也没有理由贬低应用开发的技术含量,只是从代码结构的复杂度来说,控件开发要高于应用开发。而应用开发也有应用开发的难点,比如SQL的优化,分层,部署,配置等等,很多都是控件开发人员所很少接触的弱项。也许我的措辞不当。请见谅。但是,复杂度就是复杂度。

>> 2. 复杂度是如何度量的

呵呵,整个文章只是我的一个经验总结。其中有很多东西都是很感性的。也许我真的应该写成”控件产品的开发在千行代码圈复杂度方面应该相当于一般面向特定业务定制的应用程序开发的2.5倍“。呵呵,这样写很累,不是我真正想表达的内容,所以我不会在这个事情上投入太大的精力来确保我说的每句话都是正确的。呵呵,好吧,我承认,我也不确定上面那句我思考了137秒的话是正确的。

我并不想吹嘘自己项目的规模,最后反而被人耻笑。后文我会提到为什么要说这些数据。

>> 3,代码规模与复杂度有必然联系么?

呵呵,没有。但是,代码规模却是一个说出来每个人都会有一个感性认识的东西。否则,我怎样说明我的项目的复杂度?

>> 这个用NCover跑一下就知道覆盖率结果了,很容易。

请看一下我的发帖时间,我是在家里发的。后文中也已经提到,MultiRow项目的单元测试是集成在DailyBuild中的。每天都会有单元测试的覆盖率数据出来。只是,我一直没有记住那个”数字"。但是,为了看文章的人不被我误导(我确认,我说的话不都是正确的),所以我希望能够将更多的可能的数据暴露给大家。从而让阅读的人能够有一个相对准确的认识。

本文写完的时候,我又去核对了一遍数据,我的记忆还太乐观了。实际数据代码是240K,一个模块26%(这个是核心模块),覆盖率一个模块13%,还有3个模块0%

>> “单元测试应该在物理设计阶段进行规划,而不是完成代码后”

你后面的内容说的都很有道理。在现实中,我经常可以听到有人使用“补单元测试”这样的说法。这也是我为什么将这一条放在最前面提出的原因。MultiRow项目在第一个版本中没有进行什么有效的单元测试。后来,当我看到一些成功的项目中已经在使用单元测试的时候,我意识到是单元测试是真实可行的提高产品质量的手段。应该推行单元测试了。但是,我发现,当一个系统的内部耦合度较高的时候,编写单元测试确实是一件非常困难的事情。因为,隔离被测对象的成本很高。最终导致很多复杂度相对较高的模块我无法有效的进行单元测试。从而,我得出了上面的经验。

你说的很有道理,能否拿你的项目的具体信息和实践来讨论。这样我想才能够更针锋相对。

>> “Mock类库一般情况下都是鸡肋”

我的博客发出后,很意外的没有听到太多的反对声音。当时,挺失落的,我在Twitter上就发了一句牢骚。@jeffz_cn回复反驳我说:他的项目中单元测试覆盖率在6~7成之上,并且他很好的使用了Mop库做Mock。所以,他反对我认为Mock类库是鸡肋的说法。

其实,我也认为自己写Mock对象对单元测试的组织来说是一个挑战。只是,MultiRow目前的单元测试数量还没有达到那么高度,所以我没有看到Mock 类库的价值。这也是我为什么要在一开始将项目的大概状态汇报一下的原因。因为,不同的项目状态,自然会得出完全不同的结论。

另一方面,开发人员很多时候很容易被一些抽象的,Cool的东西所吸引。而我的实践告诉我,自己写一个Mock对象的成本很多时候比学习Mock类库抽象的接口要容易很多。有时候,亚历山大之剑是解开戈迪亚斯结的最优方案。

>> 据我所知,Rhino.Mock和Nunit.Mock都具有良好的OO设计。

呵呵,Expect.On(mockObject.DoSomething()).Repeat.Once().Return(AnyThing); 这样的语句能够算良好面向对象?好吧,至少这样的语句对于大多数开发人员来说应该是挑战吧?我不知道你能否很快的告诉我,前面一段代码是什么意思?我也不知道一个一般的"静态类型语言“开发人员能不能在5分钟内写出使用Rhino.Mock做一个期待输入是偶数的Mock对象。

不要说Rhino了,即使.NET 3.5的Lambda表达式,我也是不断的向大家推荐不要使用的。

>> “静态类型语言”,4.0以前Microsoft引入动态语言的概念了么?

动态类型语言已经发展了有10年多了。JavaScript就是一个。应该说是相当成熟的一套体系了。微软早在前年就已经提出了DLR (动态类型语言运行时)的概念。IronPython的历史应该更早。注:IronPython虽然开源,但是,目前是由微软的独立团队在开发的,IronPython的架构师目前在负责DLR团队。

>> “Mock类库的接口设计往往和我们开发人员(尤其是静态类型语言开发人员)的思维方式不一致”
严重的不敢苟同。据我所知,Rhino.Mock和Nunit.Mock都具有良好的OO设计。“静态类型语言”,4.0以前Microsoft引入动态语言的概念了么?
这使我产生了一个好奇,你们的代码中会有很多类,这些类中有很多方法,这些方法是按照什么规则放在这个类上,而不是其他的类。
我的看法,如果没有OOA,就没有OOD,没有OOD,谈何OOP。

我没看出来我说Rhino.Mock的设计不面向对象和我所在的项目是否OOD有什么关系。

>> 在没有条件TDD时,单元测试往往是对“已有代码”编写的。实质性问题不是代码是否已经存在,而是你的设计不支持你去做单元测试。

Bingo,说的没错。我想表达的也是这个意思。就是:单元测试是需要设计上支持的。必须在设计时考虑设计的可测试性。一旦设计和编码完成,在进行单元测试往往会遇到很多无法逾越的障碍。我不是TDD的拥趸者。我曾经尝试过几次测试驱动,最终都放弃了。但是,我不认为TDD不可行,只是TDD对开发人员的能力要求更高。我目前还没有那样的实力。

>> 我们后期的做法是,测试方法名为:所测试的方法名+Test+序号。在测试方法的注释中描述清楚测试目的、手段、环境、填入值、预期值等等一切你希望看到的,当然这个注释规范也是架构约束的一部分。

是一种组织方式,很多时候我也会采用这样的方法。只是,还没有数据来支持我使用这种方式,我更倾向于使用更长的方法名。我个人认为这其中还有优化的空间。这一点也是随着应用场景而变化的。我就曾经见过一个项目的所有方法名(不是单元测试哦)都是IDXXX_XXX_XX的。我坚持认为那是一个不好的设计。但是,谁知道呢,也许他们的架构师有自己的考虑,觉得那个是能找到的最优解。

>> “目前很少遇到单元测试影响重构的情况” 这句话说反了吧,重构完全依赖于单元测试,

呵呵,我在文中几次提到了我曾经受伤的事情。因此,我一直在思考这个问题。也是我最担心出现的情况。但是,我还是相信,如果真的有很多单元测试的时候(当然,还需要单元测试写的不太好),单元测试成为重构的制约应该是很常见的场景。因为,你要保证高的覆盖率,必然需要写很多对接口依赖很强的测试代码。这时候,如果需要对这些接口进行重构的时候,就会发现那些测试CASE没有带来应有的价值,反而增加的重构的工作量。

>> 据我所知,你们并没有敏捷,单元测试的作用应该只会局限于质量层面

不知道你从哪里知道的,我想,你所说的“单元测试的作用仅局限于质量层面”是指MultiRow的自动测试。呵呵,如果说自动测试,我告诉你MultiRow的自动测试CASE有170K+(是数量,不是代码行数。代码行数几乎是产品代码的两倍),代码覆盖率好像在60%左右(我又记不得了,刚才没有找到当初的报告。)开发人员的单元测试目前没有指标要求。文中我也提到了指标要求的弊端。

另外,MultiRow项目目前的开发模型是FDD(Feature驱动的设计和开发),不能说严格的按照FDD的要求做了。但是,也已经摸索出一套适合 MultiRow团队的方法。并且,已经有一些数据来证明模型在不断的改进。相对来说,比起国内的大多数项目还是要敏捷吧。没有最敏捷,只有更敏捷。

>> 我并没有看到单元测试在设计上的可借鉴的作用,我只看到了你们在设计上的无奈。

呵呵,文中我提到了“可测试≈低耦合≈好的设计”是我前面结论的基础。希望我一句玩笑话没有对大家造成误导。MultiRow项目在执行中,只有一条硬性规定:每天DailyBuild必须通过。其他的都很柔性,更多的是大家的共识和积极的推动。

我相信没有人能够敢于站出来说“我的设计就是最好的设计了”,或者甚至于只是说“我的设计已经很接近最好的设计了”,因为,设计这个东西还是有一些艺术的成分在里面的。很多时候,作者都不知道自己的设计好或不好。而单元测试提供了一个很好的可测量的指标。有些时候,当我不能确定哪个设计更好的时候,我就会选择那个可测试性更好的设计。这就是我所说的对设计的可借鉴意义。

>> 与Mock相比,测试私有方法的成本可能更低。

我不认为私有方法的测试和Mock有任何替代关系。我的立论是:如果私有方法需要测试,那么他应该抽象为Strategy类,并进行测试。否则,通过公共接口应该可以进行足够的测试。目的,无非是为了避免我心中的痛(单元测试影响重构的进行)。

>> 1300个Test Case覆盖了30K的代码,测试密度达到43 Cases/K Lines

我文中所说的是CASE数量,不是代码行数,这个数量和我的预期还有很大差距。正因为代码的覆盖率比较低,因此,单元测试还没有起到预期的作用。这也是我前文提到“没有遇到Case失败”的诡异现象的原因。只不过,就在我发出本文几天后,有一天DailyBuild就报告有一个单元测试没有通过,算是了我心愿了。

>> 你要证明,不受影响,就需要提高覆盖率。而这,就是它为什么是重构的保证。

在我目前的视角看来,单元覆盖率根本不能衡量单元测试的有效性。过分强调单元测试覆盖率反而会起到反作用。如果代码已经在那里了,人的惰性会宁可多写几个没用的CASE来提高覆盖率而不是重构既有代码来提高覆盖率。还是那句话:“单元测试不要成为了重构的绊脚石”。心理学上说:当一个人面临选择的时候,他会更倾向于选择不改变现状,即使他能够看到这是一个不好的选择。

>> 感觉在项目中推广单元测试,遇到的最大阻力是成本,因此单元测试的范围定义很重要

对于单元测试,我倒没有太多的担心过它的成本。因为,我相信从长远来看,磨刀不误砍柴工。我最担心的是这个事情是否可行,我不能在自己都没有考虑清楚的情况下要求所有开发人员跟随我一起来做这件事情。其实,我文中提到的无奈还只是我平时工作中体会的一小部分。更多的感触都无法使用文字来描述出来。这也是我没有用更强硬的方式推动单元测试的一个原因。

但是,一些已有开源项目对单元测试的使用让我坚定了“单元测试是有价值的想法”。我相信,虽然目前的平台有很多限制,我的能力也有很多提高的空间。但是,只要我不断的实践,单元测试能够带给我的东西会越来越多。

最后,我真心的表示,对于应用开发,我没有任何偏见,只是认为大家侧重点不同。我也不认为复杂度高,就意味着产出大和能力高。其实上,当初我来公司面试的时候,是被应用开发部Pass掉的。我猜测,是因为我对SQL实在太陌生了。:-p

另:我发现你在提到Mock的时候,多次提到了ServiceLocator和Remoting。可能我们所说的Mock还有很大的差异。我所说的Mock是如下的情况:

类型A的实现依赖于类型B(A, B都在系统内部)。当需要对类型A进行单元测试时,为了避免类型B的错误引起类型A的单元测试失败。我需要对类型B进行Mock。实现Stub的作用。在我的实践中,还没有需要使用Remoting的时候。不知道和你说的Mock是不是同一个东西。  


Reply by "dujid", 09-08-2009, 22:38
-----------------------------------------------------

你们讨论的两个大主题,1 控件开发在感性上比应用程序难    2.单元测试如何做,Mock没有多大用

这个1就不用再讨论了吧,拿汽车跟火车比谁更难,两者各有各的难,比下去也没意思了,而且还要说“感性” 上的话,那不就更没法比了么。 (量化都没法比的话。)

话题2也是各出自各的使用场景,因为应用程序,尤其是企业应用程序的开发主要是实现业务,业务可以直接映射代码,单元测试也就可以针对业务进行测试,

控件开发不是很熟,但根据个人经验也觉得控件真的很难进行大规模全覆盖的单元测试,再加上控件的开发环境比较苛刻,用Mock确实很费劲。

(应用程序的Mock的区别只是业务。Windows Message能Mock么?不太清楚。即使能Mock也不用Mock吧,其他简单的方法多了)



这个。。。Expect.On(mockObject.DoSomething()).Repeat.Once().Return(AnyThing);

不是好的OO吧!?好像跟是否好的OO也没啥关系。。。


什么叫做好的OO设计,当你发现你的设计和上帝创造的世界是一模一样的时候,那就是一个完美的“设计”。

在我的观念里,根本没有“OO设计”这个玩意儿,每当我要进行”设计”的时候,我会先去研究,在真实世界里,上帝是如何创建同样场景的。

之所以用OO,是因为OO可以用来描述更为接近真实世界的模型,不要试图去“设计” 你眼前看到的需求,不要试图去创建所谓的“对象”,

正确做法是,理解现存世界,挖掘和抽象现存世界的关系。大多数失败的OO设计都是因为设计者刻意的去“设计”一个原本不存在于这个世界的“对象模型”,

这违背了自然法则,在不存在的对象模型上越来越复杂的进行设计,结果到最后对象的关系一团糟,无法扩展,耦合性高。(这个时候就是LS所描述的,自己都不知道自己的设计好不好)

“OO设计”更加准确的名称应该是“OO抽象”。“设计”过程是对真实世界的抽象和描述过程。

(你只能描述世界,创建世界是上帝的事情。)

(你的设计不好,是因为你没有把真实世界描述清楚。)

(这的确是一个创造艺术的过程,能否更好的抽象真实世界不是技术上的问题,是觉悟,是对真实世界的细心观察和耐心琢磨)



我扯了半天虚的东西,你们继续。。。


Reply by "robert-nan", 09-09-2009, 9:16
-----------------------------------------------------


上帝创造的世界太复杂了,很难用计算机程序实现。

目前为止,感觉单元测试对产品质量带来的好处看起来还是很小的(相对于成本,产品-->MultiRow),也许是现在写的太少,但是按理来说写一个就应该带来一个的好处,何况写了上千个。所以我觉得,问题不在于数量,而在于质量,这一千三百个中大概有七八百是功能开发完了才考虑能不能加单元测试。结果测的都是不太会出问题的点吧。但是但是我总是觉得前途是光明的(感性)。希望以后多深在早期去考虑,不管是架构设计,还是物理设计。让它发挥它应该发挥的作用。而不仅仅是个数字。

看了楼主的帖子和WantSong的回复感觉收获很大,期待WantSong的继续拍砖。

继续学习中。


Reply by "MultiRow", 09-13-2009, 19:53
-----------------------------------------------------



>> 目前为止,感觉单元测试对 产品质量带来的好处看起来还是很小的

不光Robert有这样的感觉,我也同样有这样的感觉。但是,如果仔细分析的话,会发现感觉有时候是很不确定的。之所以我有这样的感觉,其实是因为:“进行了单元测试的部分,往往设计相对较好,变动和出现Bug的时候更少”,因此,我们往往更少的去修改那部分的代码,从而有了单元测试很少失败的结果。

反过来说,那些我们很难写单元测试的部分,往往是设计不好,依赖过于复杂,隐藏的问题很多的代码。结果,这里的设计会经常的发生一些变动,修Bug的时候又难免考虑不周。导致问题层出不穷。

我想,以上两点是我有“大多数需要测试的代码写不了测试,大多数能测试的代码测试的价值不大” 这样的感觉的一个重要原因吧。

前几天和Robert讨论这个问题的时候,想到了一个点。在这里记录一下。

0 个回复

您需要登录后才可以回帖 登录 | 立即注册
返回顶部