測試的道理

在長期的程序語言研究和實際工作中,我摸索出了一些關於測試的道理。然而在我工作過的每一個公司,我發現絕大多數人都不明白這些道理,很多團隊集體性的採用錯誤的做法而不自知。很多人把測試當成一種主義和教條,進行過度的測試,不必要的測試,不可靠的測試,並且把這些錯誤的做法傳授給新手,造成惡性循環。本來目的是提高代碼質量,結果不但沒能達到目的,反而降低了代碼質量,增大了工作量,大幅度延緩工程進度。

我也寫測試,但我的測試方式比「測試教條主義者」們的方式聰明很多。在我心目中,代碼本身的地位大大的高於測試。我不忽視測試,但我不會本末倒置,過分強調測試,我並不推崇測試驅動開發(TDD)。我知道該測試什麼,不該測試什麼,什麼時候該寫測試,什麼時候不該寫,什麼時候應該推遲測試,什麼時候完全不需要測試。因為這個原因,再加上高強的編程能力,我多次完成別人認為在短時間不可能完成的任務,並且製造出質量非常高的代碼。

###測試的道理

現在我就把這些自己領悟到的關於測試的道理總結一下,其中有一些是鮮為人知或者被誤解的。

###1 不要以為你處處顯示出「重視代碼質量」的態度,就能提高代碼質量。總有些人,以為自己知道「單元測試」(unit test),「集成測試」(integration test)這樣的名詞,就很懂編程,就可以教育其他人。可惜,光有態度和口號是不解決問題的,你還必須有實戰的技巧,深入的見解和智慧,必須切實地知道應該怎麼做。代碼的質量不會因為你重視它就得到提升,也不會因為你採取了措施(比如測試,靜態分析)就一定會得到改善。你必須知道什麼時候該寫測試,什麼時候不該寫測試,需要寫測試的時候,要寫什麼樣的測試。其實,提高代碼質量唯一可行的手段不是寫測試,而是反覆的提煉自己的思維,寫簡單清晰的代碼。如果你想真的提高代碼質量,我的文章『編程的智慧』是一個不錯的出發點。

###2 真正的編程高手不會被測試捆住手腳。是的,你身邊那個你認為「不很在乎測試」的傢伙,也許是個比你更好的程序員。我喜歡把編程比喻成開賽車,而測試就是放在路邊用來防撞的輪胎護欄……

護欄有時候是很有用,可以救命的,然而一個合格的車手,絕對不會一心想著有護欄保護,測試在編程活動中的地位也應該就是這樣。優秀的車手會很快看見優雅而簡單的路徑,恰到好處地掌握速度和時機,直奔終點而去。護欄只是放在最危險的地段,讓你出了意外不要死得太慘。護欄並不能讓你成為好的車手,不能讓你取得冠軍。絕大多數時候,你的安全只有靠自己的技術,而不是護欄,你永遠有辦法可以撞死自己。測試的作用也是一樣,即使有了很多的測試,代碼的安全仍然只掌握在你的手裡。你永遠可以製造出新的 bug,而沒有測試可以檢測到它……

通常情況下,一個合格的車手是根本碰不到這些護欄的,他們心裡想的是更高的目標:快點到達終點。相比之下,一個不合格的車手,他經常撞到賽道外面去,所以在他的心裡,護欄有著至高無上的地位,所以他總是跟別人宣揚護欄的重要性。他開車的時候為了防止犯錯,要在他經過的路徑兩邊密密麻麻擺上護欄,甚至把護欄擺到賽道中間,以確保自己的轉彎幅度正確。他在護欄之間跌跌撞撞,最後只能算是勉強到達終點。鼓吹測試驅動開發的人,就是這種三流車手,這種人寫再多的測試也不可能倒騰出可靠的代碼來。

3

在程序和算法定型之前,不要寫測試。TDD 的教條者喜歡跟你說,在寫程序之前就應該先寫測試。為什麼寫代碼之前要寫測試呢?這只是一種教條。這些人其實沒有用自己的腦子思考過這個問題,而只是人云亦云,覺得這樣「很酷」,符合潮流,或者以為這樣做了別人就會認為自己是高手。實際上在程序框架完成,算法定型之前,你都不需要寫測試。如果你想知道代碼是否正確,用人工方式運行代碼,看看結果足以。

如果你發現編程初期需要保證的性質紛繁複雜,如此之多,不寫測試你就沒信心的話,那你還是想辦法先提高下基本的編程技術吧:多做練習,簡化代碼,讓代碼更加模塊化,看看我的『編程的智慧』或者『SICP'一類的東西。寫測試並不能提高你的水平,正好相反,過早的寫測試會捆住你的手腳,讓你無法自由的修改代碼和算法。如果你不能很快的修改代碼,不能用直覺感覺到它的變化和結構,而是因為測試而處處卡頓,你的頭腦裡就不能產生所謂「flow)」,就不能寫出優雅的代碼來,結果到最後你什麼也沒學會。只有在程序不再需要大幅度的改動之後,才是逐漸加入測試的時候。

###4 不要為了寫測試而改變本來清晰的編程方式。很多人為了滿足「覆蓋」(coverage)的要求,為了可以測試到某些模塊,或者為了使用 mock,而把本來簡單清晰地代碼改成更加複雜而混淆的形式,甚至採用大量 reflection。這樣一來其實降低了代碼的質量。本來很簡單的代碼,一眼看去就知道是否正確,可是現在你一眼看過去,到處都是為了方便測試而加進去的各種轉接插頭,再也無法感覺到代碼。這些用來輔助測試的代碼,阻礙了你對代碼進行直覺思維,而如果你不能把代碼的邏輯完全映射在頭腦裡(進而產生直覺),你是很難寫出真正可靠的代碼的。

有些 C# 程序員,為了測試而加入大量的 interface 和 reflection,因為這樣可以在測試的時候很方便的把一片代碼替換成 mock。結果你就發現這程序裡每個類都有一個配套的 interface,還需要寫另外一個 mock 類,去實現這個 interface。這樣一來,不但代碼變得複雜難以理解,而且還損失了 Visual Studio 的協助功能:你不再能按一個鍵(F12)就直接跳轉到方法的定義,而需要先跳到對應的 interface 方法,然後再找到正確的實現。所以你不再能夠在代碼裡面快速的跳轉瀏覽。這種方便性的損失,會大幅度降低頭腦產生整體理解的機會。而且為了 mock,每一個構造函數調用都得換成一個含有 reflection 的構造,使得編譯器的靜態類型檢查無法確保類型正確,增加運行時出錯的可能性,出錯信息還難以理解,得不償失的後果。

###5 不要測試「實現細節」,因為那等同於把代碼寫兩遍。測試應該只描述程序需要滿足的「基本性質」(比如 sqrt(4) 應該等於 2),而不是去描述「實現細節」(比如具體的開平方算法的步驟)。有些人的測試過於詳細,甚至把代碼的每個實現步驟都兢兢業業的進行測試:第一步必須做A,第二步必須做B,第三步必須做C…… 還有些人喜歡給 UI 寫測試,他們的測試裡經常這樣寫:如果你瀏覽到這個頁面,那麼你應該在標題欄看見這行字……

仔細想一下就會發現,這種作法本質上不過是把代碼(或者UI)寫了兩遍而已。本來代碼裡面明白寫著:先做A,再做B,再做C。UI 描述文件裡面明白寫著:標題欄裡面是這些內容。你有什麼必要在測試裡把它們全都再檢查一遍呢?這根本沒有增加任何可靠性:你在代碼裡會犯錯,你把同樣的邏輯換種形式再寫一遍,難道就不會錯了嗎?

這就像某些腦子秀逗的人,他出門時總是擔心門沒鎖好,關門之後要推推拉拉好幾次,確認門是鎖上了的。還沒走幾步,他仍然在懷疑門沒鎖好,又走回去推推拉拉好幾次,卻始終不能放心 :P 這種做法非但不能保證代碼的正確,反而給修改代碼製造了障礙。理所當然,你把同一段代碼寫了兩遍,每當要修改代碼,你就得修改兩次!這樣的測試就像緊箍咒一樣,把代碼壓得密不透風。每一次修改代碼,都會導致很多測試失敗,以至於這些測試都不得不重寫。本質上就是把代碼修改了兩遍,只不過更加痛苦一些。

###6 並不是每修復一個 bug 都需要寫測試。很多公司都流傳一個常見的教條,就是認為每修復一個 bug,都需要為它寫測試,用於確保這個 bug 不再發生。甚至有人要求你這樣修復一個 bug:先寫一個測試,重現這個 bug,然後修復它,確保測試通過。這種思維其實是一種生搬硬套的教條主義,它會嚴重的減慢工程的進度,而代碼的質量卻不會得到提高。寫測試之前,你應該仔細的思考一個問題:這個 bug 有多大可能會在同一個地方再次發生?很多低級錯誤一旦被看出來之後,它就不大可能在同一個地方再次出現。在這種情況下,你只需手工驗證一下 bug 消失了就可以。

為不可能再出現的 bug 大費周折,寫 reproducer,構造各種數據結構去驗證它,保證它下次不會再出現,其實是多此一舉。同樣的低級錯誤就算再出現,也很可能不在同一個地方。寫測試不但不能保證它不再發生,而且浪費你很多時間。這測試在每次 build 的時候都會消耗時間,每次編譯都因為這些測試多花幾分鐘,累積起來之後,你就發現工程進度明顯減慢。只有當發現已有的測試沒有抓住程序必須滿足的重要性質時,你才應該寫新的測試。你不應該是為這個 bug 而寫測試,而是為代碼的性質而寫測試。這個測試的內容不應該只是防止這個 bug 再次發生,而是要確保 bug 所反映出來的,之前缺失的「性質」得到保證。

###7 避免使用 mock,特別是多層的 mock。很多人寫測試都喜歡用很多 mock,堆積很多層,以為只有這樣才能測試到路徑比較深的模塊。其實這樣不但非常繁瑣費事,而且多層的 mock 往往不能產生足夠多樣化的輸入,不能覆蓋各種邊界情況。如果你發現測試需要進行多層的 mock,那你應該考慮一下,也許你需要的不是 mock,而是改寫代碼,讓它更加模塊化。如果你的代碼足夠模塊化,你不應該需要多層的 mock 來測試它。你只需要為每一個模塊準備一些輸入(包括邊界情況),確保它們的輸出符合要求。然後你把這些模塊像管道一樣連接起來,形成一個更大的模塊,測試它也符合輸入輸出要求,以此類推。

###8 不要過分重視「測試自動化」,人工測試也是測試。寫測試,這個詞往往隱含了「自動運行」的含義,也就是假設了要不經人工操作,完全自動的測試。打一個命令,它過一會就會告訴你哪些地方有問題。然而,人們往往忽略了「人工測試」。他們沒有意識到,人工去試驗,去觀察,也是一種測試。所以你就發現這樣的情況,由於自動測試在很多時候非常難以構造(比如,如果你要測試一段複雜的交互式GUI代碼的響應),很多人花了很多時間,利用各種測試框架和工具,甚至遙控 WEB 瀏覽器去做一些自動操作,花太多時間卻發現各種不可靠,沒法測到很多東西。

其實換一個思路,他們只需要花幾分鐘的時間,就可以用人工的方式觀察到很多深入的問題。過分的重視測試自動化的原因,往往在於一個不切實際的假設,他們假設錯誤會頻繁的再次發生,所以自動化了可以省下人的力氣。但是其實,一旦一個 bug 被修好,它反覆出現的機會不會很大的。過分的要求測試自動化,不但延緩了工程進度,讓程序員惱火,效率低下,而且失去了人工測試的精確性。

###9 避免寫太長,太耗時的測試。很多人寫測試,嘰裡呱啦很長一串,到後來再看的時候,他已經不記得自己當時想測什麼了。有些人本來用很小的輸入就可以測試到需要的性質,他卻總喜歡給一個很大的輸入,下意識的以為這樣更加靠譜,結果這測試每次都會消耗大量的 build 時間,而其實達到的效果跟很小的輸入沒有任何區別。

###10 一個測試只測試一個方面,避免重複測試。有些人一個測試測很多內容,結果每次那個測試失敗,都搞不清楚到底是哪個部件出了問題。有些人為了「放心」,喜歡在多個測試裡面「附帶」測某些他認為相關的部件,結果每次那個部件出問題,就發現好多個測試失敗。如果一個測試只測一個方面,不重複測同一個部件,那麼你就可以很快的根據失敗的測試,發現出問題的部件和位置。

###11 避免通過比較字符串來進行測試。很多人寫測試的時候,喜歡通過打印出一些東西,然後使用字符串比較的方式來決定輸出是否符合要求。一個常見的做法是把輸出打印成格式化的 JSON,然後對比兩個文本。甚至有人 JSON 都不用,直接就比較 printf 輸出的結果。這種測試是非常脆弱的。因為字符串輸出的格式往往會發生微小的變化,比如有人在裡面加了一個空格之類的。把這種字符串作為標準輸出,進行字符串比較,很容易因為微小的改動而使大量測試失敗,導致很多的測試需要做不必要的修改。正確的做法,應該是進行結構化的比較,如果你要把標準結果存成 JSON,那麼你應該先 parse 出 JSON 所表示的對象,然後再進行結構化的對比。PySonar2 的測試就是這樣的做法,所以相當的穩定。

###12 「測試能幫助後來人」的誤區。每當指出測試教條主義的錯誤,就會有人出來說:「測試不是為了你自己,而是為了你走了以後,以後進來的人不犯錯誤。」 首先,這種人根本沒有看清楚我在說什麼,因為我從來沒有反對過合理的測試。其次,這種「測試能幫助後來人」,其實是沒有經過實踐檢驗,站不住腳的說法。如果你的代碼寫得很亂,就算你測試再多,後來人也無法理解,反倒被莫名其妙的測試失敗給弄得更糊塗,不知道是自己錯了還是測試錯了。我已經說過了,測試不能完全保證代碼不被改錯,實際上它們防止代碼被改錯的作用是非常弱的。無論如何,後來人都必須理解原來的代碼的邏輯,知道它在做什麼,否則他們不可能做出正確的修改,就算你有再嚴密的測試也一樣。

舉一個親身的例子。我在 Google 做出 PySonar 之後,最後一個測試都沒寫。第二次我回到 Google,我的上司 Steve Yegge 對我說:「你走了之後,我改了一些你的代碼,真是太清晰,太好把握了,修改你的代碼是一種快樂!」 這說明什麼問題呢?我並不是說你可以不寫測試,但這個例子說明,測試對於後來人的作用,並不是你有些人想像的那麼大。創造清晰的代碼才是解決這個問題的關鍵。

這種怕人突然走了,代碼無法維護的想法,導致了一些人對測試過分的重視,但測試卻不能解決這種問題。相反,如果測試太繁瑣,做不必要的測試,反而容易讓員工不滿,容易走人,去加入在這方面更加有見地的公司。有些公司以為有了測試,就可以隨便打發人走,這種想法是大錯特錯的。你需要明白的一個事情是,代碼永遠是屬於寫出它的那個人的,就算有測試也一樣。如果核心人物真的走了,就算你有再多的測試也沒用的,所以解決的方法就是把他們留住!一個有遠見的公司總是通過其他的手段解決這個問題,比如優待和尊重員工,創造良好的氛圍,使得他們沒那麼快想走。另外,公司必須注意知識的傳承,防止某些代碼只有一個人理解。

案例分析

有人會疑問,我憑什麼可以給別人講這些經驗,我自己為此有什麼成功的案例呢?所以現在來講講我做過的幾個東西,以及我親眼目睹的測試教條主義者們的失敗案例。

###Google

很多人可能聽說過我在 Google 做的 PySonar。當時 Google 的隊友們戰戰兢兢,說這麼高難複雜的東西要從頭做起,幾乎是不可能的。特別是某位隊友,一開頭就吵著要我寫測試,一直吵到最後,煩死我了。他們為什麼這麼擔心呢?因為對 Python 做類型推導是非常高難度的代碼,需要相當複雜的數據結構和算法,需要精通 Python 的語義實現。

作為一個訓練有素的專家,我沒有在乎他們的咋呼,沒有信他們的教條。我按照自己的方式組織代碼,進行精密的思考,設計和推理,最終在三個月之內做出了非常優雅,正確,高性能,而又容易維護的代碼。PySonar 到現在仍然是世界上最先進的 Python 類型推導和索引系統,被多家公司採用,用於處理數以百萬計的 Python 代碼。,

如果我當時按照 Google 隊友的要求,採用已有的開源代碼,或者過早的寫了測試,別說無法在三個月的實習時間之內完成這個東西,就算折騰好幾年也沒有可能。

###Shape Security

這種思維方式最近的成功實例,是給 Shape Security 做的一個先進的 JavaScript 混淆器(obfuscator)和對集群(cluster)管理系統的改進。不要小看了這個 JS 混淆器,它的混淆能力要比 uglify 之類的開源工具強很多,也快很多。它不但包含了 uglify 的變量換名等基本功能,而且含有專門針對人類和編譯器的複雜化,使得沒人能看出一點線索這個程序到底要幹什麼,讓最先進的 JS 編譯器也無法把它簡化。

其實這個混淆器也是一種編譯器,只不過它把 JavaScript 翻譯成不可讀的形式。在這個項目中,由於失之毫釐就可以差之千里,我採用了從 Chez Scheme 編譯器學過來的,非常嚴密的測試方法。對每一個編譯器的步驟(pass),我都給它設計一些正好可以測到這個步驟的輸入代碼(比如,具有函數定義的,for循環,try-catch的,等等)。Pass 輸出的代碼,經過 JavaScript 解釋器執行,把結果跟原來程序的執行結果對比。每一個測試程序,經過每一個 pass,輸出的中間結果都跟標準結果進行對比,如果錯了就表明那個 pass 有問題,出錯的小程序會指出大概是哪一個部分出了問題。遵循小巧,不冗餘,不重複的原則,我總共只寫了40多個非常小的 JavaScript 程序。由於這些測試涵蓋了 JavaScript 的所有構造而且幾乎不重複,它們能夠準確的定位到錯誤的改動。最後,這個 JS 混淆器能夠正確的轉換像 AngularJS 那麼大的項目,確保語義的正確,讓人完全無法讀懂,而且能有效地防止被優化器(比如 Closure Compiler)簡化掉。

相比之下,過度鼓吹測試和可靠性的人,並沒能製造出這麼高質量的混淆器。其實在我進入團隊之前,裡面的兩三位高手已經做了一個混淆器,項目延續了好多個月。這片代碼一直沒能發佈給客戶用,因為它的換名部件總是會在某些情況下輸出錯誤的代碼,修改了好多次仍然會出錯。不是100%的正確,這對於程序語言的轉換器來說,是不可接受的。換名只是我的混淆器裡的一個步驟,它還包含大概十個類似的步驟,可以把代碼進行各種轉換。

在實現換名器的時候,隊友們讓我直接拿他們以前寫的換名代碼過來,把 bug 修好就可以。然而看了代碼之後,我發現這代碼沒法修,因為它採用了錯誤的思路,縫縫補補也不可能達到100%的正確,而且明顯效率低下,所以我決定自己重寫一個。由於輕車熟路,我只花了一下午的時間,就完成了一個正確的換名器,它完全符合 JavaScript 的語義,各種奇葩的作用域規則,而且結構非常簡單。說白了,這個換名器也是一種解釋器。對解釋器的深刻理解,讓我可以很容易的寫出任何語言的換名器。

不幸的是,歷史再次重演了 ;) 隊友們聽說我花一下午重寫了一個換名器,非常緊張,咋呼地跟我說:「你知道我們的換名器是花了多少個月的時間做出來的嗎?你知道我們寫了多少測試來保證它的正確性嗎?你現在一下午做出來一個新的,你如何能保證它的正確!」 我不知道他們怎麼好意思說出這樣的話來,因為事實是,他們花了這麼多個月,耗費這麼多人力,寫了這麼多的測試,做出來的換名器卻仍然有 bug,沒法用。當我把我寫的測試和幾個大點的 open source 項目(AngularJS, Backbone 等)放進他們的換名器之後,就發現有些地方出問題了,而所有的測試和 open source 項目通過我的換名器,卻得到完全正確的代碼。另外經過性能測試,我的換名器速度要快四倍的樣子。所以就像 Dijkstra 所說:「最優雅的程序往往也是最高效的。」

結束這個項目之後,我換了一個團隊(cluster團隊),這個團隊的人要好很多,低調而且幽默。Shape Security 的產品(Shape Shifter)裡麵包含一個高可靠(HA)集群管理系統,它可以通過網絡,選舉 leader,構建一個高容錯的並行處理集群。這個集群管理系統一直以來都是公司裡很複雜,卻是可靠性要求最高的一個部件,一旦出問題就可能有災難性的後果。確實,它當時可靠性非常高,從來沒出過問題。但由於歷史原因,它的代碼過度複雜而缺乏模塊化,以至於很難擴展來應付新的客戶需求。我進入這個新團隊的任務,就是對它進行大規模的簡化,模塊化和擴展,讓它滿足新的需求。

在這個項目中,由於代碼的改動幅度很大,在同事和部門領導的理解,信任和支持下,我們決定直接拋棄已有的測試,完全靠嚴格而及時的 code review,邏輯推理,推敲討論,手工試驗來保證代碼的正確。在我修改代碼的同時,一位更熟悉已有代碼的隊友一直通過 git 默默監視著我的每一次改動,根據他自己的經驗來判斷我的改動是否偏離了原來的語義,及時與我交流和討論。由於這種靈活而嚴格的方式,工程不到兩個月就完成了。改進後的代碼不但更加模塊化,更可擴展,適應了新的需求,而且仍然非常可靠。假設部門領導是「測試教條主義者」,不允許拋棄已有的測試,這樣的項目是絕對不可能如期完成的。然而在當今世界遇到這樣領導的機會,恐怕十個人裡面不到一個吧。

###Coverity

最後,我舉一個由於測試方式不當而非常失敗的案例,那就是 Coverity 的 Java 靜態分析產品。我承認 Coverity 的 C 和 C++ 分析器也許是非常好的,然而 Java 的分析器,很難說。當我進入 Coverity 的時候,同事們已經忍受了整整一年的管理層的威逼和高壓,超時過勞工作,寫出了基本的新產品和很多的測試。可是由於技術債太多,再多的測試也沒能保證產品的可靠性。

我的任務就是利用我深入的 PL 知識,不停的修補前人留下來的各種蹊蹺 bug。有些 bug 需要運行20多分鐘之後才出現,一次還看不出是怎麼回事,所以修起來非常耗時。有時候我只好趴在電腦前面養神,時不時的睜眼看看結果。Coverity 是如此的在乎測試,他們要求每修復一個 bug 你就必須寫出新的測試。測試必須能夠如實的重現 bug 的現象,修復之後測試必須能夠通過。這看似一個很在乎代碼質量的做法,然而它不但沒能保證產品的穩定可靠,而且大幅度的減慢了工程進度,並且造成員工的疲憊和不滿。

有一次他們分配給我一個 bug:在分析一個中型項目的時候,分析器似乎進入了死循環,好幾個小時都不能完成。因為 Coverity 的全局靜態分析,其實就是某種圖遍歷算法。當這個圖裡面有迴路的時候,你就必須小心,如果不問青紅皁白就遞歸進去,就可能進入死循環。避免死循環的辦法很簡單,你構造一個圖節點的集合(Set),然後把它傳遞到函數裡面作為參數。 每當訪問一個節點,你先檢查這個節點是否已經在這個集合裡,如果在你就直接返回,否則你就把這個節點加入到集合裡,然後遞歸處理這個節點的子節點。它的 C++ 代碼大概就像這個樣子:

void traverse(Node node, Set<Node>& visited)
{
    if (visited.contains(node)) {
        return;
    } else {
        visited.add(node);
        process_node(node, visited);   // 裡面會遞歸調用 traverse
    }
}

查看代碼之後我發現,代碼其實沒有進入「死循環」,而是進入了指數複雜度的計算,所以很久都不能完成。這是因為寫這函數的人不小心,或者沒有理解 C++ 的函數參數缺省是傳值(做拷貝)而不是傳引用,所以他忘了打那個「&」,所以函數被遞歸調用的時候不是傳遞原來的集合,而是做了一個拷貝。每一次遞歸調用traverse,visited 都得到一個新的拷貝,所以返回之後,visited 的值就恢復到之前的狀態,就像 node 被自動 remove 了一樣。所以這個函數仍然會在某種情況下再次訪問這個節點。這樣的代碼不會進入死循環,然而在某種特殊的圖結構下,這會造成指數級的時間複雜度(請想一下這是什麼樣的一種圖)。

本來很明顯的一個圖論算法問題,加一個「&」就修好了,手工試驗也發現問題消失了。然而 Coverity 的測試教條主義者們(包括寫出這 bug 的那人自己),吵著鬧著,嚴肅命令我必須寫出測試,構造出可以造成這種後果的數據結構,確保這個 bug 不會再重新出現。

為一個我根本不會犯的錯誤寫測試,而且它不可能再次發生,這不是很搞笑嗎?就算你寫了測試,也不能保證同樣的事情不再發生。如果你不小心漏掉「&」,下次同樣的問題還會發生,並且發生在另外的地方,而你卻沒有給那塊代碼寫測試,所以給這個 bug 寫測試,並不能防止同樣的問題再次發生。這就像一個技術不過關的賽車手,他在別人不大可能撞車的地方撞了車,然後就要求賽場在那個地方裝上輪胎護欄。可是下一次,這個車手又會在另一個其他人都不會撞車地方撞車……

稍微有點圖論常識,熟悉 C++ 基本概念的人,都不會犯這種錯誤。防止這種問題,只有靠個人的技術和經驗,而不能靠測試。防止它再次發生的最好辦法,恐怕是開個會把這個問題講清楚,讓大家理解,下次不要再犯。所以給這個 bug 寫測試,完全是多此一舉。跟隊友們講解了這個原理,他們聽了之後,彷彿什麼都沒有聽到一樣,仍然強硬的要求:「可是你還是得寫這個測試,因為這是我們的規定!你知道要是出了 bug,送一個銷售工程師去客戶那裡,要花多少錢嗎……」 無語了。

Coverity 的 Java 分析,就是經常因為這種測試教條主義,使得項目進展及其痛苦和緩慢,卻仍然 bug 百出。Coverity 的其他的問題,還包括我上面指出的,寫重複的測試,一個測試測太多東西,使用字符串比較來做測試,等等。你恐怕很難想像,一個製造旨在提高代碼質量的產品的公司,自己代碼的質量是這樣維護的 :P

##完

由於絕大多數人對測試的誤解如此之深,測試教條主義的流毒如此之廣,導致許許多多優秀的程序員沉淪在繁瑣的測試驅動開發中,無法舒展自己的長處。為了大家有一個輕鬆,順利又可靠的工作環境,我希望大家多多轉發這篇文章,改變這個行業的陋習。我希望大家在工程中理性的對待測試,而不是盲目的寫測試,只有這樣才能更好更快的完成項目。

(由於這篇文章包含了我很多年的經驗和深入的見解,希望你覺得有收穫的話為此付費。建議價格是5美元,或者30人民幣。【付費方式】)


书籍推荐