TDD并不是看上去的那么美

TDD并不是看上去的那么美

春节前的一篇那些炒作过度的技术和概念中对敏捷和中国ThoughtWorks的微辞引发了很多争议,也惊动了中国ThoughtWorks公司给我发来了邮件想来找我当面聊聊。对于Agile的Fans们,意料之中地也对我进行了很多质疑和批评。我也回复了许多评论。不过,我的那些回复都是关于中国ThoughtWorks咨询师以及其咨询的方法的。我对Agile方法论中的具体内容评价的不是很多,所以,我想不妨讨论一下Agile方法论中的具体的实践(以前本站也讨论过结对编程的利与弊)。

那么,这次就说说TDD吧,这是ThoughtWorks中国和Agile的Fans们最喜欢的东西了。我在原来的那篇文章中,我把TDD从过度炒作的技术剔除了出去,因为我还是觉得TDD有些道理的,不过,回顾我的经验,我也并不是很喜欢TDD。我这篇文章是想告诉大家,TDD并没有看上去的那么美,而且非常难以掌控,并且,这个方法是有悖论之处的

TDD简介

TDD全称Test Driven Development,是一种软件开发的流程,其由敏捷的“极限编程”引入。其开发过程是从功能需求的test case开始,先添加一个test case,然后运行所有的test case看看有没有问题,再实现test case所要测试的功能,然后再运行test case,查看是否有case失败,然后重构代码,再重复以上步骤。其理念主要是确保两件事:

  • 确保所有的需求都能被照顾到。
  • 在代码不断增加和重构的过程中,可以检查所有的功能是否正确。

我不否认TDD的一些有用的地方,如果我们以Test Case 开始,那么,我们就可以立刻知道我们的代码运行的情况是什么样的,这样可以让我们更早地得到我们实现思路的反馈,于是我们更会有信心去重构,去重新设计,从而可以让我们的代码更为正确。

不过,我想提醒的是,TDD和Unit Test是两码子事儿。有很多人可能混淆了自动化的Unit Test(如:XUnit系例)和TDD的软件开发过程。另外,可能还会有人向鼓吹“TDD让你进行自顶向下的设计方式”,对此,请参阅本站的《Richard Feynman, 挑战者号, 软件工程》——NASA的挑战者号告诉你自顶向下设计的危险性。

TDD的困难之处

下面是几个我认为TDD不容易掌控的地方,甚至就有些不可能(如果有某某TDD的Fans或是ThoughtWorks的咨询师和你鼓吹TDD,你可以问问他们下面这些问题)

  • 测试范围的确定。TDD开发流程,一般是先写Test Case。Test Case有很多种,有Functional的,有Unit的,有Integration的……,最难的是Test Case要写成什么样的程度呢。

    • 如果写的太过High Level,那么,当你的Test Case 失败的时候,你不知道哪里出问题了,你得要花很多精力去debug代码。而我们希望的是其能够告诉我是哪个模块出的问题。只有High Level的Test Case,岂不就是Waterfall中的Test环节?
    • 如果写的太过Low Level,那么,带来的问题是,你需要花两倍的时间来维护你的代码,一份给test case,一份给实现的功能代码。
    • 另外,如果写得太Low Level,根据Agile的迭代开发来说,你的需求是易变的,很多时候,我们的需求都是开发人员自己做的Assumption。所以,你把Test Case 写得越细,将来,一旦需求或Assumption发生变化,你的维护成本也是成级数增加的。
    • 当然,如果我把一个功能或模块实现好了,我当然知道Test 的Scope在哪里,我也知道我的Test Case需要写成什么样的程度。但是,TDD的悖论就在于,你在实现之前先把Test Case就写出来,所以,你怎么能保证你一开始的Test Case是适合于你后面的代码的?不要忘了,程序员也是在开发的过程中逐渐了解需求和系统的。如果边实现边调整Test Case,为什么不在实现完后再写Test Case呢?如果是这样的话,那就不是TDD了。
  • 关注测试而不是设计。这可能是TDD的一个弊端,就像《十条不错的编程观点》中所说的一样——“Unit Test won’t help you write the good code”,在实际的操作过程中,我看到很多程序员为了赶工或是应付工作,导致其写的代码是为了满足测试的,而忽略了代码质量和实际需求。有时候,当我们重构代码或是fix bug的时候,甚至导致程序员认为只要所有的Test Case都通过了,代码就是正确的。当然,TDD的粉丝们一定会有下面的辩解:

    • 可以通过结对编程来保证代码质量。
    • 代码一开始就是需要满足功能正确,后面才是重构和调优,而TDD正好让你的重构和优化不会以牺牲功能为代价。

说的没错,但仅在理论上。操作起来可能会并不会得到期望的结果。1)“结对编程”其并不能保证结对的两个人都不会以满足测试为目的,因为重构或是优化的过程中,一旦程序员看到N多的test cases 都failed了,人是会紧张的,你会不自然地去fix你的代码以让所有的test case都通过。2)另外,我不知道大家怎么编程,我一般的做法是从大局思考一下各种可行的实现方案,对于一些难点需要实际地去编程试试,最后权衡比较,挑选一个最好的方案去实现。而往往着急着去实现某一功能,通常在会导致的是返工,而后面的重构基本上因为前期考虑不足和成为了重写。所以,在实际操作过程中,你会发现,很多时候的重构通常意味着重写,因为那些”非功能性”的需求,你不得不re-design。而re-design往往意味着,你要重写很多Low-Level的Test Cases,搞得你只敢写High Level的Test Case。

  • TDD导致大量的Mock和Stub。相信我,Test Case并不一定是那么容易的。比如,和其它团队或是系统的接口的对接,或是对实现还不是很清楚的模块,等等。于是你需要在你的代码中做很多的Mock和Stub,甚至fake一些函数来做模拟,很明显,你需要作大量的 assumption。于是,你发现管理和维护这些Mock和Stub也成了一种负担,最要命的是,那不是真正的集成测试,你的Test Case中的Mock很可能是错的,你需要重写他们。

也许,你会说,就算是不用TDD,在正常的开发过程中,我们的确需要使用Mock和Stub。没错!的确是这样的,不过,记住,我们是在实现代码后来决定什么地方放一个Mock或Stub,而不是在代码实现前干这个事的。

  • Test Case并没有想像中的那么简单。和Waterfall一样,Waterfall的每一个环节都依赖于前面那个环节的正确性,如果我们没有正确的理解需求,那么对于TDD,Test Case和我们的Code都会的错的。所以,TDD中,Test Case是开发中最重要的环节,Test Case的质量的问题会直接导致软件开发的正确和效率。而TW的咨询师和Agile的Fans们似乎天生就认为,TDD比Waterfall更能准确地了解需求。如果真是这样,用TDD进行需求分析,后面直接Waterfall就OK了

另外,某些Test Case并不一定那么好写,你可能80%的编程时间需要花在某个Test Case的设计和实现上(比如:测试并发),然后,需求一变,你又得重写Test Case。有时候,你会发现写Test Case其实和做实际设计没有差别,你同样要考虑你Test Case的正确性,扩展性,易读性,易维护性,甚至重用性。如果说我们开发的Test Case是用来保证我们代码实现的正确性,那么,谁又来保证我们的Test Case的正确性呢?编写Test Case也需要结对或是Code review吗?软件开发有点像长跑,如果把能量花在了前半程,后半程在发力就能难了。

也许,TDD真是过度炒作的,不过,我还真是见过使用TDD开发的不错的项目,只不过那个项目比较简单了。更多的情况下,我看到的是教条式的生硬的TDD,所以,不奇怪地听到了程序员们的抱怨——“自从用了TDD,工作量更大了”。当然,这也不能怪他们,TDD本来就是很难把控的方法。这里送给软件开发管理者们一句话——“当你的软件开发出现问题的时候,就像bug-fix一样,首要的事是找到root cause,然后再case by case的解决,千万不要因为有问题就要马上换一种新的开发方法”。相信我,大多数的问题是人和管理者的问题,不是方法的问题。

全文完,转载请注明作者和出处,请勿用于商业用途

(转载本站文章请注明作者和出处 宝酷 – sou-ip ,请勿用于任何商业用途)

好烂啊有点差凑合看看还不错很精彩 (34 人打了分,平均分: 4.15 )
Loading...

TDD并不是看上去的那么美》的相关评论

  1. Test Case应该是跟需求挂钩的, 一个Use Case对应多个Test Case,
    针对每一个重要场景设计一个Test Case。
    对于一个Use Case来说,重要的场景是确定的,那么主要的Test Case数量应该是确定的。
    那么再细粒度的Test Case去测试一些非重要的场景,那得根据项目情况而定了。
    需求人员关注的哪些Tast Case被实现了,至于开发人员是去实现那些Test Case。
    你说代码实现要不要设计呢?那肯定是要的,这跟用不用TDD是无关的。

  2. 被InfoQ引过来的。我以为用TDD开发的程序至少能够保证比较容易测试,也比较容易产生高聚合低耦合的设计。

    最近在维护一个C++写的遗留系统。每次测试,必须先在另一台机器上修改3个XML,运行一个程序,生成一个二进制的文件;然后将二进制文件作为我真正测试程序的输入。而且这个倒霉的程序运用了多次fork,通过配置文件制定so的文件名,进行动态加载,我折腾了半天也没搞定如何debug,只能靠日志进行调试,开发效率非常低。由此可见一个容易进行测试的系统还是很重要的。

    “测试代码难以维护”,在《xUnit Test Patterns》(http://xunitpatterns.com/) 讨论的蛮详细了。其实就是对测试代码也需要很好的组织和设计。

    “需求变化”,敏捷开发的目的就是拥抱变化嘛。TDD更容易产生高聚合低耦合,更容易适应需求的变化。

  3. “Unit Test won’t help you write the good code” 这是一种能力,没有什么方法可依赖。我觉得unittest还是很不错的东西,可以让自己的思路比较清楚

  4. 我不懂TDD,所以不对文章观点评论
    我真的是很看不惯有些人觉得什么好就奉为经典,容不得别人说半点不好

    就算lz说的不全对,但是TDD真的就是完美的?
    发表点不同观点就成异教徒了?

    曾经在某个网站上学习设计模式
    那个站长也是一个极端主义者,一味鼓吹架构,设计模式,DDD,认为数据结构算法都没用,还说什么现在大学计算机专业(注意不是软件工程)教的东西都没用……
    所以果断离开

  5. bob :>>相信我,大多数的问题是人和管理者的问题,不是方法的问题。这句话是经典,,,

    呵呵,我觉得,方法的产生就是为了协调人和管理者在项目中的分工和角色,如果方法不能做到,那他可能是不合适,但如果全部推到人的因素上,那未免有些偏颇,如果人都是完美的,那任何方法都能做好了,也就无所谓何种方法了。

  6. 如果说我们开发的Test Case是用来保证我们代码实现的正确性,那么,谁又来保证我们的Test Case的正确性呢?

    深有同感。对于做测试开发的人来说,这是个非常具有讽刺意味的一句话。我们保证另外一个人写的功能运行正确,却无法保证我们自己写的代码一定没问题。这就像是一条蛇咬住了自己的尾巴。

    1. 有个想法,tdd 的前提是把需求梳理清楚,任务分解到足够小,那么这个测试代码应该是简单的,一看就明白,也就不需要专门证明它的正确性了。

  7. 目前,TDD确实是一个比较时髦的名称,当我们这些码农还没想好怎么去做的时候,公司的测试组已经开始针对需求进行Test Case的开发,曰介凑是TDD啊。开发项目组内也曾讨论过如果更有效的利用在开发过程中写的针对于各个接口的Unit Test,试着组织起一套完整的自动化测试过程出来。但是我们项目的性质,更多需要测试的实际上是Functional,Integration, System Test,简单的Unit Test仅仅能保证的是个人代码能正常运行而已。并且随着项目的进展,可能会是其他人来对这个功能进行重构,从接口上进行变化是一件很常见的事情,往往到最后,这些实现的Unit Test只能束之高阁,也许哪天需要底层调试的时候再重新改一改运行一下而已。所以对于我个人而言,我宁可相信从测试组传道过来的TDD理念,而不是由Developer完全Driver的TDD,前者往往能帮助你发现你系统中的更多问题。

  8. TDD中的T可以是任何类型的测试,Unit Test, Integration Test, 或者 Functional Test都可以。

  9. 评论不知道怎么没了?再发,希望不是发重了。

    楼主的观点我不大赞同。

    TDD我理解应该是分层次的,需求、设计、编码的人都可以以TDD的方式工作和协作,各个层次的人通过不同层次的测试用例表达了自己想要的是什么。因此,TDD不是一个人写了一堆测试代码然后以此驱动所有人干活。

    最早写出来的测试代码应该直接针对业务功能的,需求人员即使不能写也可以读一下这些测试代码,确认一下这些测试代码是不是代表了他们想要的业务功能特性。这些测试代码对应的是需求。

    为了实现这些功能,做设计的人会设计一些模块或者方法,这些模块和方法可能是他自己写也可能要求别人写,于是他又写了一些测试代码,用来表明他要求实现的方法是什么样的。这些测试代码对应的是概要设计。

    被要求实现这些方法的人,为此可能要再做一些更细致的设计(编码本来就是设计),如果继续用TDD的方法,那么他们也会写一些测试用例来检验自己的代码。这些测试代码对应的是一些私有方法和实现代码了,可以认为是单元测试或者白盒测试代码。

    另外说到方法和流程的问题,无疑项目是依靠人的,如果有足够的几个相互熟悉的牛人来,也许什么方法都不需要的。但现实情况下,每个项目你都得不到足够的牛人,你面对的就是一群资质很一般、经验很一般的普通人。即使你在google这类地方,我相信也是有少数人(通常这些少数人就是项目领头人什么的)认为其他大部分人相对他们面临的任务是笨蛋。搞出这些“方法”来,无非就是让一群普通人不至于弄出什么不好收拾的纰漏来,平安地把项目做完。

    东方文明与西方文明的差距越来越大,我认为根本原因之一是在于东方文明太不注意理论和方法的积累,感性、人治的成分为主。依靠明君而不是依靠制度总不是很靠谱的事情。

  10. 我很虔诚的对待TDD,确实收获很多, 尤其自动化测试, 是人工测试无法比拟的。
    还有小步快跑的思想, 测试保证了改动的正确。

    我顶TDD

  11. TDD的核心是将大问题分解为正交的小问题,设计出正交的系统,它的能力范围仅限于这里,不要以银弹的视角攻击它。
    维护测试代码需要成本,但没有这些代码成本更大,TDD赌的就是这一条,如果人人都是天才,TDD就不需要了。

    1. 赞同,顶!TDD解决了不了所有问题,不能说解决不了所有问题它就是没价值的。TDD是有成本,所以有成本我们就不干。

  12. 我是TDD的忠实fans, 我不认同你在文中提到的“关注测试而不是设计”, TDD,在一定层度上使用意向编程,写出来的代码是可测的,易读的的及且易维护的,至于”TDD导致大量的Mock和Stub”,我想说的是,现在有很多Mock框架,大大减少了这类代码(如java的mocikto),TDD的好处不用说, 一个可以提高生产率的技术, 是值得大家去克服困难的!

  13. 我还不会TDD,嘿嘿,会都不会、用都不用,所以,我没有资格去评价TDD,一定要先学会,并能说出一二三,再回来,嘻嘻

  14. @黄英杰
    。所以你就需要在两者之间平衡。你是需要进行粒度更大的测试来保证系统的稳定性的时候,项目进度,testcase实现的工作量,维护的工作量是会加倍的增加。同时如果你的testcast出现错误的时候,你也会觉得很恐怖,因为你会得到一个你一直以为对,但确实错的成功。同时testcase无法发现你系统中的一个很小的问题。举个例子,在某个函数模块中你需要从配置库中读取配置,读取不到读取默认值。单单一个这样的功能,你就需要使用编写两个testcase来验证它。所以当测试粒度到达一定程序细的时候,你会发现你需要比你实现代码花3倍以上的时间来实现它,维护他就不算了。
    测试我是觉得一定需要的,不过究竟测试应该到达一个怎么样的粒度,应该在什么时候写testcase,个人认为每个项目都根据它自身的实际而有不同。不过取舍不是一件容易的事。
    以上只是本人的一些浅见,用于交流,都希望测试使得程序更好

  15. 只要可以以“所有、全部”这类词儿当作TDD的噱头,以为TDD是纠结什么测试覆盖率的,那么你就完全不懂TDD。

    TDD是一个开发方法,就好比如你当前开发10万行代码中(估计)的30行代码,你写了一个有4行代码的TDD作为驱动行为,这叫做什么“全部”。

  16. 相信我,大多数的问题是人和管理者的问题,不是方法的问题——不能同意更多

  17. 我和TW合作过敏捷项目,不过在我个人的理解中,TDD中测试和设计是统一而非矛盾的。我印象中敏捷软件开发的原则、模式和实践中提到过“隐喻”,认为这是最重要也是最容易被忽略的 一点。我理解的“隐喻”就是全局视图,就是整体架构的设计了,而且还是分层的,每个子系统每个模块有自己的“隐喻”视图。之所以为”隐喻”我想也是强调最终的架构还是需要多次迭代才能清晰可见。
    我认同TW提出的TDD中的’T’是功能测试,并且认为明确的是针对的“隐喻”视图中的每个接口的功能的测试,这是开发和测试工作的起点,既然“隐喻”是分层的,开发和测试工作自然也是分层的。
    我理解的敏捷,无论任何实践,软件设计是基础,否则敏捷软件开发:原则、模式和实践中就不会探讨那么多的软件设计原则了。

  18. 我用TDD很多年了,我承認目前在下代碼生成速度比是我當菜鳥第一年的1/5
    這是缺點,我時時刻刻都在懷疑TDD是否需要
    如果公司是用代碼生成量計算績效,一定死的妥妥的

    我很久沒有debug我的程式,bug少到QA和客戶抓不出來,都是debug別人的代碼
    這是個小優點
    為了這個優點,我還是忍耐的走在TDD的道路上

    TDD需要大量練習,它不是萬能
    要寫出漂亮的mock和stub也不容易

    過於不及都不好
    事先做出好的預先設計,再用TDD修正細部功能我認為是不錯的

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注