浏览 21544 次
锁定老贴子 主题:TDD, Cache
该帖已经被评为精华帖
作者 正文
时间:2006-09-06
首先明确一点:TDD中的设计是指代码而不是头脑中的预期软件模型.
所以TDD的意思就是围绕测试产生代码.

为什么要围绕测试?
1.写测试的过程就是对code不断的review.
2.还有很多的好处...
   
0 请登录后投票
时间:2006-09-13
tianxinet 写道
认可:
1、写必要的test case提高单元测试的质量;
2、写test code确实有助于理清设计;
3、test code有助于提高单元测试的效率;
4、test code能提高对于单元测试的信心,也就是code-world内的信心。

目前确定认可的只有以上几项。

疑问:
1、测试先行,或者把写test case的过程当作“思考和浮现设计”的途径,成本一定低吗?
2、test code一定有利于重构吗?比如有时当重构funcation code的时候也要重构大量的test code很爽么?
3、“测试先行”对整个开发过程来说提高了效率吗?
4、大量的test code或很高的coverage rate真的能提高对一个应用的信心吗?极致也就是单元测试做到足够好甚至“超”好吧,没有real-world的反馈,这信心靠的住吗?(除非做的东西主要是给自己爽)
5、过多的test code或过高的coverage rate有些时候是不是有些“神经质”了?开公交需要和F1一样全副武装来保护吗?(尽管开公交的责任更大)

目前我对以上所有疑问的答案都是否。
另请看这哥们在重构“Maven's assembly plugin”时怎样被test suite愚弄的:
http://forum.javaeye.com/viewtopic.php?t=22187
或原文:
Testing: Coverage Reports Considered Dangerous

“过度设计”的时候是对开发人员的不信任,“过度测试”同样体现了对开发人员的不信任。
纯粹的“测试先行”导致的“过度测试”其实是有内心深处的恐惧感的?因为完全抛弃掉了“传统的设计先行”,由此带来一些问题,于是想通过“过度测试”来弥补?同样危机在不同的方法中只是被转移了而不是被消除了?

很久没来参加讨论,没想到有这么多的讨论了,赫赫,翻阅了一遍,还是觉得这个问题我最想作针对的讨论。

引用
1、测试先行,或者把写test case的过程当作“思考和浮现设计”的途径,成本一定低吗?


无论是否TDD,设计和实现的成本总是免不了的。 引用martin fowler的原话说,如果成本仅仅
指代码的话,或许TDD成本相对来说高一些。

TDD对于设计是一种促进的途径,但是思考设计的实现和TDD没有严格的先后顺序,你可以先用
草图或者充沛的头脑构思粗略的设计(详细的设计谁能够用头脑完全想出来? ),这点和是否TDD没有关系,然而重要的区别是,在开始code的时候,是直接写实现代码呢? 还是以Test code反映你的已经在脑子构思的设计呢? 后者,就是我现在所做的TDD。

记得,前面很久的时候,partech发出一个很大的疑问(不知道,他现在是否还是困惑中?),他问: 一个很大的很复杂的功能,如果使用TDD的话,如何展开设计呢?

我个人的回答是: 讨论,讨论,分析,分析 。。。,得出的结果是:清晰的功能需求列表,TODO list 。
得到TODO list后,对大的复杂的任务模块作合理的分解,展开一些预先的设计和分析,可以画一些草图,UML。 得到大体的设计思路之后,就可以以TestCode推进设计的实现。

其实,前面的步骤和所有的软件开发方式基本无甚区别,总是需要一个分析,讨论的过程。

引用
2、test code一定有利于重构吗?比如有时当重构funcation code的时候也要重构大量的test code很爽么?


对于TestCode,除了回答你这个问题之外,我还想对说一些废话:

TestCode不一定有利于重构,这是肯定的。因为TDD的推行是一个和开发者经验密切相关的产物。
过渡的TestCode明显就是一个累赘,在重构的时候,这种累赘就会得到反映,正如你这个问题的提出。

我认为对于测试代码的编写有一个警觉度,当为了验证一个模块的功能,却需要编写非常复杂的测试验证代码的时候,
对于这样的测试逻辑就需要一个反问的过程:真的需要这么复杂吗?
如果真的要这么复杂,那么后面的改动就会更复杂。

千方百计地编写一个自认为可以验证功能的复杂的测试代码,是不值得提倡的。
或许在上面的 反问:“真的需要这么复杂的测试逻辑吗?” 之后加一个肯定句:只要这么简单的测试就可以保证了。



最后你的那个问题确实很经典:有时当重构funcation code的时候也要重构大量的test code很爽么?

答案是肯定不爽的,在这个时候,很多人都归结为TDD的责任。

我先从一个封装的角度来看待这个有意思的场景:重构funcation code的时候也要重构大量的test code。

function code的重构会影响大量的测试,也就意味着大量的TestCode耦合于这个functionCode的逻辑,从这点来看,这本身就反映了设计存在的问题。为什么设计的缺陷会在编写了大量地和function 实现耦合的测试代码之后才得到发现呢? 或许原因可以在 “是否进行了频繁的代码重构? ”这样的反问中得到解答。

假设一个function需要几个逻辑点来组成,每一个逻辑点做了相应的封装,每个逻辑点都有相应的测试代码。

无论针对这个Function的整体的测试,还是针对每个逻辑点的测试,这些测试总是针对其各自的“实现”作相应的测试保证。

如果小步骤地重构,所修改的代码部分仅会影响到一小部分测试代码 。当然,如果你的重构不是小步骤前进,而是大踏步冲锋,把整体的逻辑顺序全部改掉,每一个逻辑点的实现都全部改掉,然后跑测试自然全部error。
重构总是应该频繁的进行的,Code Review是促进重构的一个重要发起 点,或许团队在每个固定小周期内对整体代码的review是设计和代码在朝着良好方向发展的保证之一。

记得Martin Fowler在他的“设计已死”这篇论文提到:

The fundamental assumption underlying XP is that it is possible to flatten the change curve enough to make evolutionary design work. This flattening is both enabled by XP and exploited by XP.
This is part of the coupling of the XP practices: specifically you can't do those parts of XP that exploit the flattened curve without doing those things that enable the flattening. This is a common source of the controversy over XP. Many people criticize the exploitation without understanding the enabling. Often the criticisms stem from critics' own experience where they didn't do the enabling practices that allow the exploiting practices to work. As a result they got burned and when they see XP they remember the fire.

就我的见解是:
”测试(UnitTest和持续集成测试)“和”频繁重构”两者结合的结果是:大量的单元测试和集成测试代码,自动化的测试套件,持续演进的实现代码。当需求变化时,因为有上面三者的保证,所需要的开发成本就非常小。
这是TDD的理想目标,也是kent beck和martin们所津津乐道的终极目标,然而,在具体的开发中,很多团队都忽略了频繁的重构,
忽略了设计的演进,同时也因为“过度单元测试”的问题,使得项目的整体规划失去了方向。


3 和 4 的问题,其实在上面2的回答里面,都顺便回答了。
   
0 请登录后投票
时间:2006-09-13
看了两遍,做了下总结:
1.回答了TDD需要分析过程,结果是明确的todolist
2.提到了测试代码的味道对重构的反馈作用
3.从封装角度回答了良好TDD的测试代码的形式,需要小步重构
4.最后提供了TDD开发过程的必备过程

对于回答1,分析能到多深,因人而异,TDD需要分析多深,个人觉得TDD者自认为能重构出来,能演进设计,所以他们通常不会考虑很多。
对于回答2,我觉得还可以总觉出很多测试影响重构的技巧来(或者书都谈的比较清楚了,关键是应用而已),当然技巧的应用也因人而异,相信是可以练出来的。
对于回答3,觉得可能很受开发者经验影响,是努力的方向。
对于回答4,TDD一定要必备这些过程,值得付出。

头脑发晕啊。
   
0 请登录后投票
时间:2006-09-14
仔细读了firebody的回复不下三遍,又把该讨论所有帖子从头到尾读了一遍

firebody 写道
引用
1、测试先行,或者把写test case的过程当作“思考和浮现设计”的途径,成本一定低吗?


无论是否TDD,设计和实现的成本总是免不了的。 引用martin fowler的原话说,如果成本仅仅
指代码的话,或许TDD成本相对来说高一些。

TDD对于设计是一种促进的途径,但是思考设计的实现和TDD没有严格的先后顺序,你可以先用
草图或者充沛的头脑构思粗略的设计(详细的设计谁能够用头脑完全想出来? ),这点和是否TDD没有关系,然而重要的区别是,在开始code的时候,是直接写实现代码呢? 还是以Test code反映你的已经在脑子构思的设计呢? 后者,就是我现在所做的TDD。

记得,前面很久的时候,partech发出一个很大的疑问(不知道,他现在是否还是困惑中?),他问: 一个很大的很复杂的功能,如果使用TDD的话,如何展开设计呢?

我个人的回答是: 讨论,讨论,分析,分析 。。。,得出的结果是:清晰的功能需求列表,TODO list 。
得到TODO list后,对大的复杂的任务模块作合理的分解,展开一些预先的设计和分析,可以画一些草图,UML。 得到大体的设计思路之后,就可以以TestCode推进设计的实现。

其实,前面的步骤和所有的软件开发方式基本无甚区别,总是需要一个分析,讨论的过程。

这样的TDD实践我没有疑问,而且会力挺。或者说这种实践是以TDD为基本过程,并加入了其它过程的一些元素、自己的一些最佳实践。(我是这样理解的)

我的几个疑问都基于自己这样一个认识:我不喜欢任何“纯粹的(pure)”过程,或者说我没觉得目前有任何一种过程可以包治百病。对任何一种过程、方法的“神话”都会让我有天然的抵触或警觉(这要么是情绪驱动、要么是利益驱动)。

关于设计成本,所有敏捷过程关于设计的理念,都得基于一个“敏捷团队”(要素是技能、经验),否则成本只会高不会低(当然这是废话了:)),如果是一个敏捷团队,是有眼见的实际好处的。如果团队不敏捷(只有带队人敏捷是不行的),我觉得不要贸然采用敏捷过程的设计理念,先让团队锻炼到具备敏捷的基本要素,然后再推行TDD。

firebody的回复其实还涉及了我另外一个疑问:“纯粹的测试先行”(或者说把写test code作为唯一的设计手段)是否可行,我认为不太可行,至少“性价比”不高(因为它太pure,这在理念推广阶段是可以的,矫枉不免过正)。要是我,会选择test code、设计交叉或并行的方法,会参考firebody的实践。
如果我的理解都是正确的,那么这部分没有问题了。



firebody 写道
引用
2、test code一定有利于重构吗?比如有时当重构funcation code的时候也要重构大量的test code很爽么?


对于TestCode,除了回答你这个问题之外,我还想对说一些废话:

TestCode不一定有利于重构,这是肯定的。因为TDD的推行是一个和开发者经验密切相关的产物。
过渡的TestCode明显就是一个累赘,在重构的时候,这种累赘就会得到反映,正如你这个问题的提出。

我认为对于测试代码的编写有一个警觉度,当为了验证一个模块的功能,却需要编写非常复杂的测试验证代码的时候,
对于这样的测试逻辑就需要一个反问的过程:真的需要这么复杂吗?
如果真的要这么复杂,那么后面的改动就会更复杂。

千方百计地编写一个自认为可以验证功能的复杂的测试代码,是不值得提倡的。
或许在上面的 反问:“真的需要这么复杂的测试逻辑吗?” 之后加一个肯定句:只要这么简单的测试就可以保证了。



最后你的那个问题确实很经典:有时当重构funcation code的时候也要重构大量的test code很爽么?

答案是肯定不爽的,在这个时候,很多人都归结为TDD的责任。

我先从一个封装的角度来看待这个有意思的场景:重构funcation code的时候也要重构大量的test code。

function code的重构会影响大量的测试,也就意味着大量的TestCode耦合于这个functionCode的逻辑,从这点来看,这本身就反映了设计存在的问题。为什么设计的缺陷会在编写了大量地和function 实现耦合的测试代码之后才得到发现呢? 或许原因可以在 “是否进行了频繁的代码重构? ”这样的反问中得到解答。

假设一个function需要几个逻辑点来组成,每一个逻辑点做了相应的封装,每个逻辑点都有相应的测试代码。

无论针对这个Function的整体的测试,还是针对每个逻辑点的测试,这些测试总是针对其各自的“实现”作相应的测试保证。

如果小步骤地重构,所修改的代码部分仅会影响到一小部分测试代码 。当然,如果你的重构不是小步骤前进,而是大踏步冲锋,把整体的逻辑顺序全部改掉,每一个逻辑点的实现都全部改掉,然后跑测试自然全部error。
重构总是应该频繁的进行的,Code Review是促进重构的一个重要发起 点,或许团队在每个固定小周期内对整体代码的review是设计和代码在朝着良好方向发展的保证之一。

记得Martin Fowler在他的“设计已死”这篇论文提到:

The fundamental assumption underlying XP is that it is possible to flatten the change curve enough to make evolutionary design work. This flattening is both enabled by XP and exploited by XP.
This is part of the coupling of the XP practices: specifically you can't do those parts of XP that exploit the flattened curve without doing those things that enable the flattening. This is a common source of the controversy over XP. Many people criticize the exploitation without understanding the enabling. Often the criticisms stem from critics' own experience where they didn't do the enabling practices that allow the exploiting practices to work. As a result they got burned and when they see XP they remember the fire.

就我的见解是:
”测试(UnitTest和持续集成测试)“和”频繁重构”两者结合的结果是:大量的单元测试和集成测试代码,自动化的测试套件,持续演进的实现代码。当需求变化时,因为有上面三者的保证,所需要的开发成本就非常小。
这是TDD的理想目标,也是kent beck和martin们所津津乐道的终极目标,然而,在具体的开发中,很多团队都忽略了频繁的重构,
忽略了设计的演进,同时也因为“过度单元测试”的问题,使得项目的整体规划失去了方向。


3 和 4 的问题,其实在上面2的回答里面,都顺便回答了。

仔细读过后,这部分我只有一个疑问了,就是关于“频繁重构”(也应该是细粒度重构或“小步重构”)。两点:
a)愿不愿意“频繁重构”,或者说是否认可“频繁重构”(细粒度、“小步”)的好处;
b)有没有“频繁重构”的环境。
a)没问题。就是b)了。

我举的那个重构“Maven's assembly plugin”的例子,我又想了想,又想了一下实际的项目--如果大量funcation code都被重构了,也就是说大量老的代码都被抛弃了,那么抛弃原来的test code也是理所当然的。问题在于原来投入的大量努力被抛弃掉了(机会成本,任何设计理念都会有这个问题)。但这种环境毕竟存在,比如需求的频繁变更(或产品理念的重大变更),任何已经固化的设计都可能会被迫抛弃,带来“机会成本”的损失。

既然都有这个问题存在,为什么还有疑问呢?

问题在于一个关键短语“设计的固化”(为了讨论方便这样说),传统的设计方式先“固化”设计,“照单(设计)定制”,产出物是设计文档 + funcation code;后来XP的出现,淡化了设计的先行“固化”,代码才是最终“固化”的设计,这里的代码是funcation code--唯一的产出物;而TDD的出现似乎又强化了设计的“固化”,当然不是先行,而是不断迭代进行,test code是最终“固化”的设计,产出物是test code + funcation code。似乎只有XP的没有“冗余的设计”。

那么XP、TDD在设计理念这点上,有什么优劣么?有说看test code这样的“设计”比看funcation code这样的设计更清晰明了。我不赞同这样的说法,这很牵强,要想看清晰明了的设计,设计文档最好。所以这并不是TDD的优势,这也是我倾向于把TDD看作开发手段而不是设计方法的原因之一。如果把test code看作设计,那它就是“冗余的”,如果把它看作质量保证手段、开发手段、辅助的设计手段,它是优秀的。

当然,用不用TDD不是问题,而是怎样用。firebody明确指出了一个惊天秘密--“经验”,精辟呀。不立足于经验谈TDD就是无根之水、浮沙上筑高台,就是误人子弟,就可能支出更多的成本
   
0 请登录后投票
时间:2006-09-14
引用
仔细读过后,这部分我只有一个疑问了,就是关于“频繁重构”(也应该是细粒度重构或“小步重构”)。两点:
a)愿不愿意“频繁重构”,或者说是否认可“频繁重构”(细粒度、“小步”)的好处;
b)有没有“频繁重构”的环境。
a)没问题。就是b)了。


如果把“频繁重构”的重要性上升到“设计过程”的关键部分,那么我想这个“愿不愿意”的问题就不存在了。

在一个复杂的需求多变的项目中, 任何人都不可能预先把所有的详细设计作完,大家都是从一个设计的草图展开工作的。设计的实现是逐渐展开/演进的,而不是一步到位的。

code的重构来源于: 1)新的需求对原来的FunctionCode的设计提出更改的需求 2)代码的“坏味道” 。

至于B),不知道你所说的环境具体指什么,我自己认为环境就是:
1)IDE对重构的支持 赫赫重构的支持 Eclipse和IDEA都支持得非常好

2) 自动集成测试的环境--TW的cruise control。


引用
我举的那个重构“Maven's assembly plugin”的例子,我又想了想,又想了一下实际的项目--如果大量funcation code都被重构了,也就是说大量老的代码都被抛弃了,那么抛弃原来的test code也是理所当然的。问题在于原来投入的大量努力被抛弃掉了(机会成本,任何设计理念都会有这个问题)。但这种环境毕竟存在,比如需求的频繁变更(或产品理念的重大变更),任何已经固化的设计都可能会被迫抛弃,带来“机会成本”的损失。

既然都有这个问题存在,为什么还有疑问呢?

问题在于一个关键短语“设计的固化”(为了讨论方便这样说),传统的设计方式先“固化”设计,“照单(设计)定制”,产出物是设计文档 + funcation code;后来XP的出现,淡化了设计的先行“固化”,代码才是最终“固化”的设计,这里的代码是funcation code--唯一的产出物;而TDD的出现似乎又强化了设计的“固化”,当然不是先行,而是不断迭代进行,test code是最终“固化”的设计,产出物是test code + funcation code。似乎只有XP的没有“冗余的设计”。

那么XP、TDD在设计理念这点上,有什么优劣么?有说看test code这样的“设计”比看funcation code这样的设计更清晰明了。我不赞同这样的说法,这很牵强,要想看清晰明了的设计,设计文档最好。所以这并不是TDD的优势,这也是我倾向于把TDD看作开发手段而不是设计方法的原因之一。如果把test code看作设计,那它就是“冗余的”,如果把它看作质量保证手段、开发手段、辅助的设计手段,它是优秀的。

当然,用不用TDD不是问题,而是怎样用。firebody明确指出了一个惊天秘密--“经验”,精辟呀。不立足于经验谈TDD就是无根之水、浮沙上筑高台,就是误人子弟,就可能支出更多的成本


大量重构FunctionCode,这样的场景发生的话,确实比较可怕, 能够发生的这样的灾难的case,我所能想到有几个:

1) 一些基本的需求 分析错误

2) 缺乏频繁的重构,代码泥团逐渐增长

对于1)的避免,已经有一大堆实践原则,比如现场客户,实时交流等等。。。

无论TDD,还是别的开发方式,都是对这种错误作最努力的“避免” 。

在实际开发中,2)倒是最容易犯的。
   
0 请登录后投票
时间:2006-09-14
firebody 写道

至于B),不知道你所说的环境具体指什么,我自己认为环境就是:
1)IDE对重构的支持 赫赫重构的支持 Eclipse和IDEA都支持得非常好

2) 自动集成测试的环境--TW的cruise control。


我的意思是指外部环境逼迫进行“大步的重构”,比如需求的变更或产品理念的变更(最糟糕的就是需求不断的变更)。“小步频繁重构”是好的,有时不得不被迫进行“大步重构”。加上这个补充,上边的帖子内容就更连贯一些。
   
0 请登录后投票
时间:2006-09-14
tianxinet 写道
firebody 写道

至于B),不知道你所说的环境具体指什么,我自己认为环境就是:
1)IDE对重构的支持 赫赫重构的支持 Eclipse和IDEA都支持得非常好

2) 自动集成测试的环境--TW的cruise control。


我的意思是指外部环境逼迫进行“大步的重构”,比如需求的变更或产品理念的变更(最糟糕的就是需求不断的变更)。“小步频繁重构”是好的,有时不得不被迫进行“大步重构”。加上这个补充,上边的帖子内容就更连贯一些。

大步的重构,如果你这里指的具体场景是“ 新的需求的提出 对于原有的设计需要做作出大的变更”的话,我觉得这确实在项目非常复杂的无法避免,我也经常碰到这样的问题,因为需求的分析和考虑总是欠缺的,有时候也需要一个对简单的需求推动设计的实现的“迫切”愿望 。

我个人认为此时的“大步伐重构”或许称之为“设计的大步伐演进”更合适一些,比如一些隐语在你前期的讨论中根本不可能考虑到,只有经过前期的代码的积累,才可能会暴露这些隐语出来,这些隐语对于促进系统设计的清晰,简洁具有十分关键的作用。

大步伐的重构,我的经验是把大步伐的重构细分为几个小步伐的重构,保证改动代码后影响的测试代码的失败可以在短时间内得到修复。同时,大规模的重构也需要一个规划,列出todo list,在具体的分析中捕捉重构的思路。
   
0 请登录后投票
时间:2006-12-13
ajoo 写道
gigix 写道
我怎么感觉布娃娃对TDD的理解有点不着边际呢?
TDD是什么?你只需要回答一个问题:你在写代码之前要不要先想清楚写什么?如果你回答“是”,那么TDD就是要求你先把“要写什么”这件事情想清楚、记下来,回头写完了代码之后再验证一下你写的是不是你想要的。
所以我就不明白,这TDD怎么就能跟什么速度、什么冲锋手/狙击手扯上关系了?你要愿意坐那先琢磨半小时然后再开工,没人拦着你——很多任务本来就需要深思熟虑的。但不管你是琢磨半小时先还是拿来就开跑,都请先说清楚,你到底想干个啥——请写测试。至于说重构,你要是真能一开始就想清楚了回头就不再重构,当然也是大好事一件,TDD除了给你一个说明和验证自己想法的途径,它哪儿妨碍你精心设计了?


我有一个问题一直想请教敏捷专家.

我写东西的时候,很少有写代码之前就知道自己到底要什么东西.(真正的目标只是大致知道,但是还不知道最终的api是什么样子)
都是在写代码-发现味道-修改的过程中,最终的api才浮现出来。当然不是所有的东西都写好才能写测试,但是我的测试都是在代码一定程度成型,总体框架设计基本稳定之后才写的。

先写测试,总有一种没地方下嘴的感觉。觉得就算勉强想了一个接口,回头想法一变,还要从头再写,不经济。

而且一个一个小模块,细粒度地写测试也比较累。我写的单元测试粒度一般相对较粗,曾经被敏捷的拥趸认为更象集成测试。我的想法是在能达到一定的代码覆盖的基础上最小化花在测试上的时间。只要粒度的尺寸还在我可以良好控制的范围内就可以。

当然,模块设计还是要保证单元测试的友好性。不过是用自己的嗅觉来保证的。如果在需要重构的时候发现test case不够,再加。而不是刚开始就为了预想中的重构而大规模写test case。

test driven肯定是能够提高代码质量,控制项目进度等好处的。但是也有代价的。猜想会不会这里面有个曲线,在某个程度上收益/成本最高。而这个最优点不见得在最极端的test first的那个地方?而也许这个点对不同的个人也是不一样的?


我的设计不是测试驱动。测试仍然是保证代码质量,提供信心的工具,但仅此而已。


第一次顶老帖,请各位原谅。我觉得ajoo的做法是正确的,测试一个模块的功能,而不是掰开来测,即便你没有用mock来掰开它们。我觉得入手的测试,应该是一个直觉上是一个模块的功能性级别的测试。然后写很简单情况下的测试,比如只有一些边界条件。由于测试使用的数据是简单的情况,实现就可以很简单。或者实现压根就是测试数据的硬编码。然后再去变换测试数据,迫使实现变得更加复杂。这个时候往往会出现一些细粒度的接口,当内部情况变复杂了。再去写一些针对具体细粒度接口的测试。我觉得功能测试完全没问题,不一定得分开来测。
   
0 请登录后投票
时间:2007-01-02
看完感觉buaawhl似乎在抱怨firebody在jpa里面TDD中的Test要求过于细致了“对一个问题的过分关注”..:)

我觉得在Service层对每个service统一处理TC就够了,但随着AOP、filter等东东,似乎不够,想完整的测试一个系统似乎比写一个系统麻烦多了...
   
0 请登录后投票
论坛首页 软件开发和项目管理版 TDD

跳转论坛:
JavaEye推荐