|
最近CoolShell上的一篇《TDD并不是看上去的那么美》引起了敏捷社區(qū)的高度關(guān)注和激勵(lì)辯論。今天,InfoQ甚至專門舉行了一個(gè)虛擬座談會(huì)《TDD有多美?》,幾位國(guó)內(nèi)敏捷社區(qū)的名人專門就此問(wèn)題展開了深入地討論。不論結(jié)果如何,這種探討和反思的精神還是非常值得贊賞的。事件實(shí)際上可以簡(jiǎn)單地歸納為一個(gè)有一定影響力的開發(fā)人員質(zhì)疑TDD,一群敏捷社區(qū)名人對(duì)TDD進(jìn)行解釋和辯護(hù)。現(xiàn)在,就讓我堅(jiān)定地站在CoolShell一邊,為對(duì)TDD的質(zhì)疑和批判添磚加瓦吧!
我們首先來(lái)看看TDD的核心理念是什么。第一是用例即規(guī)范(Specification by Example),即把測(cè)試用例作為需求規(guī)范的一種形式。傳統(tǒng)的需求表達(dá)方式包括文檔,Use Case等,而TDD強(qiáng)調(diào)通過(guò)測(cè)試用例來(lái)表達(dá)需求。另外,TDD的測(cè)試用例是黑盒的基于外部接口的,所以,它實(shí)際上又是對(duì)外部接口的設(shè)計(jì)。不把測(cè)試用例單純地視為測(cè)試,而從需求和設(shè)計(jì)的角度來(lái)看測(cè)試用例是TDD與傳統(tǒng)測(cè)試的一個(gè)重要區(qū)別。TDD的第二個(gè)重要理念是Test First,強(qiáng)調(diào)測(cè)試對(duì)于實(shí)現(xiàn)的驅(qū)動(dòng)作用,先寫測(cè)試用例,再實(shí)現(xiàn)和重構(gòu)。Test First的實(shí)質(zhì)是先理解清楚需求,并做好外部接口設(shè)計(jì),把它轉(zhuǎn)化為測(cè)試用例,然后再來(lái)實(shí)現(xiàn)和重構(gòu)。
如果說(shuō)用例即規(guī)范還彌補(bǔ)了文檔和Use Case在表達(dá)需求時(shí)的某些不足,具有一定的好處,那么Test First則有很大的問(wèn)題,尤其在沒(méi)有測(cè)試用例失敗之前,不要寫任何一行代碼的極端方式則更是極端的錯(cuò)誤。
如果測(cè)試用例就是需求和設(shè)計(jì),那么為什么不能先寫出測(cè)試用例再來(lái)實(shí)現(xiàn)呢?這不是我們最熟悉的先需求再設(shè)計(jì)再編碼嗎?答案是:不能執(zhí)行的測(cè)試用例(Test First)和能執(zhí)行的測(cè)試用例有著天壤之別,你寫出了測(cè)試用例不代表你就看到了運(yùn)行的實(shí)際效果。
不能執(zhí)行的測(cè)試用例和寫在紙上的文檔相比對(duì)實(shí)現(xiàn)的指導(dǎo)意義不見得能好到哪里去!除非是一些很簡(jiǎn)單的情況下,在實(shí)際的軟件開發(fā)中,你很難在沒(méi)有執(zhí)行測(cè)試用例的情況下寫出真正符合最終需求的測(cè)試用例來(lái)。比如:你做一個(gè)頁(yè)面,頁(yè)面的效果需求和設(shè)計(jì)通常會(huì)在真正可以運(yùn)行之后不斷調(diào)整,在實(shí)現(xiàn)之前只能有一個(gè)大致的輪廓和方向,許多方面的細(xì)節(jié)要么是沒(méi)想清楚,要么是完全沒(méi)想到,不可能一蹴而就。如果片面強(qiáng)調(diào)測(cè)試對(duì)實(shí)現(xiàn)的驅(qū)動(dòng)作用,那么實(shí)際上隱含了需求和設(shè)計(jì)的細(xì)節(jié)可以在實(shí)現(xiàn)之前明確下來(lái)的假設(shè),這是非常不敏捷的和不現(xiàn)實(shí)的!
Test First要求寫測(cè)試用例時(shí)對(duì)軟件需求有精確的了解,但實(shí)際軟件開發(fā)過(guò)程中用戶需求和外部環(huán)境的不確定性會(huì)導(dǎo)致軟件需求難以把握和頻繁變動(dòng)。
用戶需求的不確定性是指需求無(wú)法在用戶真正能運(yùn)行看到效果之前明確下來(lái)。比如:讓你開發(fā)一套Wow這樣大型的游戲,你能想象游戲的效果是設(shè)計(jì)者一開始就想好了精確到每一個(gè)細(xì)節(jié)嗎?對(duì)于游戲這樣的軟件,需求和設(shè)計(jì)不可能脫離實(shí)際運(yùn)行紙上談兵地產(chǎn)生。游戲的設(shè)計(jì)者通常只能借助文檔、草圖、Use Case等非精確的方式大致提出需求,先做出原型,在看到效果之后才能逐步地細(xì)化和明確,需求設(shè)計(jì)的增加和改變會(huì)伴隨整個(gè)軟件開發(fā)過(guò)程。
另外,還有一種極端的情況是根本不存在精確的用戶需求,比如:自動(dòng)化翻譯軟件,你能在實(shí)現(xiàn)之前就把翻譯效果用測(cè)試用例固定下來(lái)嗎?存在絕對(duì)正確的翻譯方法嗎?最近,我們和國(guó)外一家大公司客戶談一個(gè)項(xiàng)目需求的時(shí)候,客戶講了這樣一句話我們現(xiàn)階段還無(wú)法提出很細(xì)致的需求,只有等你們拿出第一個(gè)版本,然后我們?cè)僦鸩降卣{(diào)整細(xì)化。我們的客戶沒(méi)有宣稱自己在做敏捷,但人家的思維方式多敏捷啊,不是什么一上來(lái)就明確需求,而且還要精確寫出自動(dòng)化測(cè)試用例。有人說(shuō)這種情況我們?nèi)匀豢梢韵雀鶕?jù)自己的理解進(jìn)行TDD,這樣做可以:
1. 基于測(cè)試用例和客戶溝通明確需求;
2. 驅(qū)動(dòng)實(shí)現(xiàn)。我對(duì)此持不同看法,能執(zhí)行的測(cè)試用例和不能執(zhí)行的測(cè)試用例有著天壤之別,客戶從測(cè)試用例根本無(wú)法獲得真實(shí)運(yùn)行的體驗(yàn),你能想象蘋果把iPhone的測(cè)試用例寫在PPT上,給用戶做一個(gè)演講,用戶就能給出關(guān)于iPhone設(shè)計(jì)的反饋了嗎?要真正的用戶反饋,就需要實(shí)打?qū)嵉能浖@不正是敏捷的Working software over document的思想嗎?另外,既然用戶無(wú)法在實(shí)際體驗(yàn)之前提出反饋,那么開發(fā)人員在開發(fā)初期做的需求分析和設(shè)計(jì)都只是一個(gè)探索,隨時(shí)可能調(diào)整甚至被推翻,不值得在實(shí)現(xiàn)之前進(jìn)行自動(dòng)化測(cè)試設(shè)計(jì)的投資。
外部環(huán)境的不確定性是指"當(dāng)我們的系統(tǒng)需要和外部系統(tǒng)集成時(shí),關(guān)于外部系統(tǒng)行為的假設(shè)也無(wú)法在實(shí)際集成運(yùn)行前完全確定"。例如,要做一套股票客戶端連上交易所系統(tǒng),因?yàn)榻灰姿男袨闀?huì)直接影響到客戶端的開發(fā),所以只有在弄清交易所行為的情況下才談得上開發(fā)出高質(zhì)量的客戶端。如果采用測(cè)試驅(qū)動(dòng),編寫了各種涉及交易所行為的測(cè)試用例,比如什么情況下發(fā)什么類型的消息,消息格式如何,如何交互等等,但是這些測(cè)試用例本身是否正確卻需要打一個(gè)大大的問(wèn)號(hào)!這一方面是由于很多交易所提供的協(xié)議都不夠清晰或者有許多未明確定義的地方;另一方面即使協(xié)議沒(méi)有問(wèn)題,開發(fā)人員也可能由于單純的失誤或者缺乏相應(yīng)領(lǐng)域的基本知識(shí)而把協(xié)議理解錯(cuò)。
實(shí)際上,要真正弄清交易所的行為明確客戶端的需求,最重要的手段還是在交易所提供的測(cè)試環(huán)境中跑集成測(cè)試。對(duì)于Test First來(lái)講,測(cè)試用例本身的錯(cuò)誤可以說(shuō)是代價(jià)最大的,不僅浪費(fèi)時(shí)間和精力,更重要的是還打擊開發(fā)人員的士氣,誰(shuí)愿意來(lái)回折騰呢?但很不幸,實(shí)際情況是在最初沒(méi)有明確交易所行為的時(shí)候Test First出來(lái)的測(cè)試用例隨時(shí)可能在真實(shí)集成后被推翻,并且如果是比較高層的需求分析失誤,那對(duì)整個(gè)架構(gòu)設(shè)計(jì)來(lái)講會(huì)是災(zāi)難性的后果。在實(shí)際開發(fā)中,我們的軟件需要和其他系統(tǒng)集成的情況是非常普遍的,而期望在沒(méi)有進(jìn)行實(shí)際集成的情況下弄清外部系統(tǒng)的行為都是不現(xiàn)實(shí)和不敏捷的。
所以,Test First需要對(duì)于被測(cè)系統(tǒng)的需求和環(huán)境有精確的了解,但由于需求不確定性和外部環(huán)境不確定性兩大問(wèn)題,Test First在很多時(shí)候都是不現(xiàn)實(shí)的。其實(shí),Test First和瀑布式思想一脈相承,都強(qiáng)調(diào)需求先于實(shí)現(xiàn),而忽略了軟件需求的產(chǎn)生會(huì)受到實(shí)現(xiàn)的反饋,會(huì)在實(shí)際運(yùn)行中不斷調(diào)整探索完善。TDD無(wú)非是把需求分析的結(jié)果用測(cè)試用例表達(dá),替代傳統(tǒng)用文檔表達(dá)需求,但從宏觀上看,TDD和瀑布比是換湯不換藥,這都不是真正的敏捷。
除了簡(jiǎn)單情況,不存在脫離實(shí)現(xiàn)的需求,你能夠在明確了需求之后就實(shí)現(xiàn)出一套Linux系統(tǒng)嗎?既然你根本無(wú)法實(shí)現(xiàn)一套Linux系統(tǒng),那么這樣所謂的需求又有多大的意義呢?所以,能提出什么樣的需求不能脫離你的實(shí)現(xiàn)能力。需求和實(shí)現(xiàn)之間不是簡(jiǎn)單的誰(shuí)驅(qū)動(dòng)誰(shuí),而是一種相互反饋的關(guān)系,這與需求用什么方式表達(dá)沒(méi)有關(guān)系。正如瀑布模型無(wú)法在初始階段做出完美的需求分析,TDD也無(wú)法在初始階段做出完美的測(cè)試用例;不僅如此,自動(dòng)化測(cè)試用例的開發(fā)維護(hù)成本還遠(yuǎn)高于文檔。
所以,在敏捷環(huán)境中,軟件開發(fā)初期應(yīng)該通過(guò)文檔和用例等手段大致表達(dá)需求,實(shí)現(xiàn)之后在實(shí)際運(yùn)行中體驗(yàn)效果,不斷優(yōu)化探索和明確需求和外部環(huán)境,當(dāng)需求和對(duì)外部環(huán)境的認(rèn)識(shí)達(dá)到一個(gè)比較穩(wěn)定的程度才編寫測(cè)試用例將需求固化下來(lái)。
上面的論述主要針對(duì)貼近最終用戶的外部需求(如ATDD),下面我會(huì)進(jìn)一步解釋即使是在內(nèi)部的單元測(cè)試級(jí)別TDD仍然有問(wèn)題。我們還是首先從需求入手,思考一下單元的需求是哪里來(lái)的呢?答案是:需求來(lái)自于設(shè)計(jì)!比如,對(duì)輪胎的需求來(lái)源于汽車的設(shè)計(jì),低層模塊的需求來(lái)源于高層模塊的設(shè)計(jì)。
而在開發(fā)初期,這種內(nèi)部設(shè)計(jì)具有很大的不穩(wěn)定性,帶有很多假設(shè)的成分,在沒(méi)有進(jìn)行集成測(cè)試的情況下,很難講這種內(nèi)部設(shè)計(jì)是否合理。實(shí)際項(xiàng)目開發(fā)通常會(huì)在集成運(yùn)行之后不斷調(diào)整內(nèi)部的設(shè)計(jì),即影響單元的需求。那么,如果是測(cè)試驅(qū)動(dòng),首先按不成熟的內(nèi)部設(shè)計(jì)把一個(gè)個(gè)單元需求編寫成單元測(cè)試再來(lái)實(shí)現(xiàn),實(shí)際上大大推遲了能進(jìn)行集成測(cè)試的時(shí)間,對(duì)于真正快速弄清高層需求穩(wěn)定設(shè)計(jì)反而是不利的。
假設(shè)最終還是所有單元都完成,然后開始運(yùn)行集成或驗(yàn)收測(cè)試,這時(shí)候有兩種可能:
1. 用戶看到實(shí)際效果,決定調(diào)整需求;
2. 發(fā)現(xiàn)集成前在單元層面的假設(shè)不成立或者是有沒(méi)有考慮到的情況。不論是哪一種情況發(fā)生,以前所寫的單元測(cè)試都面臨著被廢棄或必須修改的命運(yùn)。實(shí)際上,多數(shù)與業(yè)務(wù)相關(guān)的單元測(cè)試用例比起集成或驗(yàn)收測(cè)試用例更加不穩(wěn)定,因?yàn)樗鼤?huì)受到所有其上層模塊的需求和設(shè)計(jì)變動(dòng)的影響。
由于我們?cè)诓环€(wěn)定的單元測(cè)試上浪費(fèi)了大量的時(shí)間(按我的經(jīng)驗(yàn)編寫單元測(cè)試比編寫實(shí)現(xiàn)更耗時(shí)),這就導(dǎo)致了遲遲無(wú)法進(jìn)行集成看到實(shí)際效果,也沒(méi)有辦法敏捷地應(yīng)對(duì)需求的調(diào)整。也就是說(shuō)具有諷刺意味的,Test First理念居然是和敏捷理念矛盾的!
所以,我認(rèn)為Test First不符合敏捷開發(fā)的基本假設(shè),而真正符合敏捷的理念是需求和設(shè)計(jì)依賴于實(shí)現(xiàn)的反饋,需要在實(shí)際運(yùn)行過(guò)程中根據(jù)效果不斷探索調(diào)整得來(lái)的,不可能脫離實(shí)際運(yùn)行寫出真正符合最終需求的測(cè)試用例來(lái)。所以,我們真正應(yīng)該做的是盡快看到實(shí)際運(yùn)行的效果,而自動(dòng)化測(cè)試作為固化的需求和設(shè)計(jì)是在看到效果之后。在集成之前花太多精力進(jìn)行測(cè)試驅(qū)動(dòng)只會(huì)導(dǎo)致遲遲看不到實(shí)際運(yùn)行效果(尤其是基于開發(fā)人員自己的假設(shè)編寫大量單元測(cè)試用例),看到效果需要調(diào)整需求又會(huì)廢掉或改掉一大堆的測(cè)試用例。
實(shí)際上,越是外部的需求其變更帶來(lái)的影響和代價(jià)越大,越是需要盡早明確。從宏觀上看,TDD所謂的快速反饋實(shí)際上是加快內(nèi)部反饋,延遲了外部反饋,這無(wú)異于本末倒置。而大量需要修改或作廢的測(cè)試用例其實(shí)是一種很大的浪費(fèi),這和消除浪費(fèi)的精益思想也是矛盾的!

上面這幅cost/length_of_feedback_cycle圖是我們常見的用于說(shuō)明敏捷方法比傳統(tǒng)方法具有更短的反饋周期,更小代價(jià)的應(yīng)對(duì)變化。從圖中我們可以清晰的看到在驗(yàn)收測(cè)試中發(fā)現(xiàn)的需求錯(cuò)誤導(dǎo)致的代價(jià)是最高的。如果驗(yàn)收測(cè)試往后推遲一點(diǎn),發(fā)現(xiàn)錯(cuò)誤的代價(jià)將按非線性地增長(zhǎng)。上面我們已經(jīng)論述了,任何方法都不可能消除驗(yàn)收測(cè)試后對(duì)需求的調(diào)整,因?yàn)檫@是需求產(chǎn)生的正常過(guò)程。
我們唯一可以做的是盡可能地縮短驗(yàn)收測(cè)試的反饋周期,但是很不幸TDD大量的內(nèi)部測(cè)試只會(huì)導(dǎo)致推遲驗(yàn)收測(cè)試的時(shí)間,從而大大增加代價(jià)。在實(shí)際開發(fā)中,我提倡在第一次集成運(yùn)行測(cè)試之前不要寫單元測(cè)試用例;自動(dòng)化的驗(yàn)收測(cè)試用例則視編寫和維護(hù)的代價(jià)而定,如果代價(jià)比較高,則應(yīng)該采用文檔和Use Case來(lái)描述需求,因?yàn)檫@兩種方式比自動(dòng)化的驗(yàn)收測(cè)試更容易維護(hù)。編寫單元測(cè)試一定是在集成以后,這樣才能首先得到外部反饋,盡量先保證做正確的事情,再正確地做事。
下面這段話來(lái)自于InfoQ文章《Mock不是測(cè)試的銀彈》:在使用JMock框架后測(cè)試編寫起來(lái)更容易,運(yùn)行速度更快,也更穩(wěn)定,然而出乎意料的是產(chǎn)品質(zhì)量并沒(méi)有如我們所預(yù)期的隨著不斷添加 的測(cè)試而變得愈加健壯,雖然產(chǎn)品代碼的單元測(cè)試覆蓋率超過(guò)了80%,然而在發(fā)布前進(jìn)行全面測(cè)試時(shí),常常發(fā)現(xiàn)嚴(yán)重的功能缺陷而不得不一輪輪的修復(fù)缺陷、回歸 測(cè)試。為什么編寫了大量的測(cè)試還會(huì)頻繁出現(xiàn)這些問(wèn)題呢? 這描述的情況和我在實(shí)踐中遇到的情況類似,不過(guò)很可惜文章并沒(méi)有找到問(wèn)題真正的原因。it知識(shí)庫(kù):TDD到底美不美?,轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。