談“測試驅動的開發”
現在的很多公司,包括 Google 和我現在的公司 Coverity,都喜歡一種“測試驅動的開發”(test-driven development)。它的原理是,在寫程序的時候同時寫上自動化的“單元測試”(unit test)。在代碼修改之后,這些測試可以批量的被運行,這樣就可以避免不應該出現的錯誤。
這不是一個壞主意。我在 Kent 的編譯器課程上也使用了很多測試。它們在編譯器的開發中是不可缺少的。編譯器是一種極其精密的程序,微小的改動都可能帶來重大的錯誤。所以編譯器的項目一般都含有大量的測試。
但如果使用不當,測試卻會大幅度的降低開發效率。當我給 Google 開發 Python 靜態分析的時候,我幾乎沒有使用任何測試。雖然組里的成員催我寫測試,但是我卻知道那只會降低我的開發效率。因為這個程序在幾個星期的過程中,被我推翻重來了好幾次。要是我一開頭就寫上測試,這些測試就會礙手礙腳,阻礙我大幅度的修改代碼。
最后的結果是,我在 12 個星期之內,寫出了 Google 一個小組的人需要幾年才做得出來的東西。現在這個東西里面的技術,仍然處于世界領先地位。就連 Coverity 這種專門做靜態分析軟件的公司,代碼質量也無法與之相比。按照他們的開發方式,要想在 12 個星期之內做出這個東西,是完全不可能的事情。
測試的另一個副作用是,它讓很多人對測試有一種盲目的依賴心理。改了程序之后,把測試跑一遍沒出錯,就以為自己的代碼是正確的。可是測試其實并不能保證代碼的正確,即使完全“覆蓋”了也是一樣。覆蓋只是說你的代碼被測試碰到過了,可是它在什么條件下碰到的卻沒法判斷。如果實際的條件跟測試時的條件不同,那么實際運行中仍然會出問題。唯一能可靠的確保代碼正確的方法是使用嚴密的邏輯推理,證明它的正確。
很多人寫程序只是憑現象來判斷,而不能精密的分析程序的邏輯,所以他們修改程序經常“治標不治本”。如果程序出問題了,他們的辦法是看看哪里錯了,也不怎么理解,就改一下讓它不再出錯,最多再把所有測試跑一遍。或者再加上一些新的測試,用以保證這個地方下次不再出問題。
這種做法的結果是,程序里出現大量的“特殊情況”和“補丁”。把一個“蟲子”按下去,另一個蟲子又冒出來。忙活來忙活去,最后仍然不能讓程序滿足“所有情況”。其實能夠“覆蓋所有情況”的程序,往往比能夠“覆蓋特殊情況”的程序簡單很多。這是一個很奇怪的事情:能做的事越多,代碼量卻越少。也許這就叫做程序的“美”。
美的程序不可能從修修補補中來。它必須完美的符合事物的本質,否則就會出現上面的情況,有許許多多無法修補的特例。程序員跟畫家其實差不多,畫家如果一天到頭蹲在家里,肯定什么好東西也畫不出來。程序員也一樣,蹲在家里面對電腦,其實很難寫出什么好的代碼。你必須出去觀察事物,尋找“靈感”,而不只是修改代碼。在修改代碼的時候,你必須用“心靈之眼”看見代碼背后的事物。這也是為什么很多高明的程序員不怎么用調試器(debugger)的原因。他們只是用眼睛看著代碼,然后閉上眼,腦海里浮現出其中信息的流動,所以他們經常一動手就能改到正確的地方。