[ZZ]程序員爲什麼不寫單元測試

 
筆記曾經做過一次“程序員在項目開發中編寫單元測試的情況”的調查。調查結果:
1.       嚴格的在項目中執行TDD 幾乎沒有
2.       爲大部份業務方法編寫單元測試,並保證方法測試通過。 佔16.6%
3.       偶爾編寫單元測試,一般情況下不寫。 佔58.3%
4.       爲了應付項目檢查而寫單元測試,但並不保證方法是否測試通過。 佔8.3%
5.       從來不編寫單元測試。佔16.6%
因爲調查具有一定的侷限性或片面性,調查結果並不十分精確。也基本能夠反映國內程序員編寫單元測試的狀況。很少有程序員能夠比較認真的去編寫單元測試。那麼到底又是什麼原因呢?根據筆者參與的多個討論,主要有下面幾種原因使程序員不編寫單元測試。
  • 1.       爲了完成編碼任務,沒有足夠的時間編寫單元測試。編寫單元測試會導致不 能按時完成編碼任務,推遲項目進度。
  • 2.       單元測試的價值不高,完全是浪費時間。
  • 3.       業務邏輯比較簡單,不值得編寫單元測試。
  • 4.       不知道怎麼編寫單元測試。
  • 5.       項目沒有要求,所以不編寫
  • 6.       在項目的前期還是儘量去編寫單元測試,但是越到項目的後期,就越失控。
 
測試常常是程序員十分厭倦的一個項目活動。測試能夠爲我們帶來什麼?瞭解這些非常的重要。測試不可能保證一個程序是完全正確的,但是測試卻可以增強我們對程序的信心,測試可以讓我們相信程序做了我們期望它做的事情。測試能夠使我們儘量早的發現程序的bug.
一個bug被隱藏的時間的越長,修復這個bug的代價就越大。在《快速軟件開發》一書中已引用了大量的研究數據。指出:最後才修改一個bug的代價是在bug產生時修改它的代價的10倍。
在這裏,我們需要討論的重點是單元測試。單元測試是一個方法層級上的測試,單元測試也是最細粒度的測試。用於測試一個類的每一個方法都已經滿足了方法的功能要求。
在現代軟件開發過程中,不管是xp還是rup都是十分重視單元測試。已經把單元測試作爲貫穿整個開發週期的一項重要的開發活動。特別是在現代軟件開發過程中,有經常集成和漸近提交的方法論。總結出了非常好的單元測試理論和實踐:
  在編寫代碼之前先編寫單元測試,即測試先行
  單元測試是代碼的一部份,所有的代碼必須有單元測試,並使測試通過。(像在Spring這些優秀的開源項目中在這方面做出了非常好的例子)
  在修改代碼之前先修改單元測試,並使它測試通過。
 
在編寫代碼之前先編寫單元測試,會帶來非常多的好處:
  在編寫代碼之前先編寫單元測試,並不是編寫代碼之前需要一次性爲所有的類都事先編寫單元測試。這需要有一個粒度 的把握。最大的粒度應該控制在一個類級別上,最合適的粒度是控制在一個方法級別上。先爲某一個方法編寫測試代碼,然後再爲該方法編寫實現代碼,直到其測試 通過後再爲另一個方法編寫測試代碼,如此循環。單元測試在這裏已經是一個契約規範了,它規範了方法應該做什麼,實現什麼。測試代碼遠遠要比難以閱讀和不會 及時更新的需求文檔更有價值。
 測試先行,鼓勵對需求的理解。如果沒有理解需求,你是不可能寫出測試代碼的,當然你也不可能寫出好的實現代碼。
  測試代碼與其它文檔相比會更有價值。當需求發生改變,實現代碼也相應改變。而往往需求文檔,設計文檔得不到及時更新。測試代碼相比那些過期的文檔更具有價值。
  測試先行可以編寫出最大覆蓋率的測試代碼。如果在方法的實現代碼編寫完後再編寫測試代碼,這時開發人員總是編寫一個正確路徑的測試代碼。它已經很難全面的去分析其它分支邏輯。
  如果我們採用測試先行,那麼就自動的完成了爲所有的類都編寫測試這個實踐原則。爲所有的類都編寫測試會將爲你帶來非常多的好處:
  我們可以很好的使用自動化測試來測試所有的類,特別是採用日構建的系統。
   可以讓我們放心的爲類或方法添加新的功能。我們可以很容易的修改測試代碼並驗證修改後的代碼是有用的代碼。
   可以讓我們放心的對代碼進行重構和進行設計優化。重構和設計優化通常會關聯到多個類及多個方法。如果我們爲所有的類都編寫了測試,我們就可以在重構代碼後很輕鬆的進行測試我們的修改是否正確。
   爲所有的類編寫測試,可以讓我們很容易的修改bug。當接到一個bug報告後,我們總是先修改測試代碼,然後修改實現代碼,使測試成功。這樣不會因爲修改一個問題而造成新問題的產生。
良好的單元測試策略給我們增強了對程序的信心,減少了bug的產生及bug的潛伏期。降低修改bug的代價。單元測試不會是項目開發週期的某一個生命週期,它貫穿於項目的整個生命週期,是一個非常重要的日常開發活動。
 
我們已經知道了單元測試是多麼重要的。爲什麼程序員仍然不編寫單元測試呢?爲什麼程序員總是有理由拒絕編寫單元測試呢?
一、編寫單元測試,增加了工作負擔,會延緩項目進度?
   這是筆者在多次討論和調查中程序員拒絕編寫單元測試的最多理由。“爲了完成編碼任務,沒有足夠的時間編寫單元測試。編寫單元測試會導致不能按時完成編碼任務,推遲項目進度”。事實上真的是這樣的嗎?軟件有着其特殊的生命週期,軟件開發也具有特殊性。
首先,我們需要提供給用戶的至少是一個能運行的產品。絕對不能是一堆不能運行的和充滿了異味的啞代碼。只有能夠運行的,滿足客戶需求的代碼纔是真正有用的代碼。這時代碼就變成產品了。
很多程序員只關注編寫代碼的完成時間,而乎略了調試代碼,集成及修改和維護時間。
如果沒有單元測試,開發活動會是這樣的情景。
以一個web應用開發爲例:業務代碼編寫完成->打包->發佈到服務器->進行功能測試->發現問題->修改代碼->再打包……如此循環。
任何一個web程序員對於這種開發情景都不會感到陌生。往往不斷的打包,發佈,功能測試的時間是代碼編寫的10倍以上。通過集成系統來發現程序的bug,我們往往很難一下子準確的定位bug產生的地方。應用服務器提供的錯誤信息對於我們來說是非常有限的。
如果爲每一個類都編寫單元測試並讓每一個方法測試通過,又會是怎麼樣的開發情景呢?
編寫測試代碼->編寫業務代碼->運行測試方法->修改代碼讓測試通過->所有的類都通過測試->打包->發佈到服務器->進行功能測試->發現bug->修改測試代碼->修改業務代碼->測試通過->再打包…如此循環。
從上面的過程顯而易見,我們需要花費更多的編碼時間。因爲需要爲每一個業務類編寫 測試代碼。但是它並不會導致我們總體需要花費更多的時間。我們只是可以非常輕鬆的在ide環境中運行測試方法。在代碼尚未打包發佈之前我們就已經確保了業 務代碼的正確性。當我們把所有通過測試的代碼集成到應用服務器後,出現錯誤的機率要少得多。當集成測試後發現bug時,我們也總是先修改測試類。保證在集 成之前所有的類都經過測試通過。這樣功能測試的時間就成數量級的減少,所以總的花費時間要比沒有單元測試要少得多。
 另外,如果沒有單元測試,會經常出現一些低級的錯誤,如拼寫錯誤,空指針異常等。就因爲一個小小的拼寫錯誤而需要重新打包,發佈一次。如果有單元測試,就可以避免這些低級的錯誤。
如果沒有單元測試,把代碼集成到應用服務器後再發現錯誤時,我們往往更多的是憑藉自己的經驗來判斷問題出在哪裏。對於沒有經驗的程序員來說只能是撞運氣了。這就像是瞎子走路一樣,兩眼一把黑。如果每個類都有單元測試,就無需要這麼痛苦了。
這使得我回想起當年做網絡系統。當時的局域網絡都是採用環狀網絡,還沒有現在的交換機來組星形網絡。環狀網絡的傳輸網絡採用同軸細纜線,網絡中的所有節點都在一條主幹線上,網絡的兩端都會加上一個電阻來形成一個環。
環狀網絡的最大的缺點就是當任意一個節點有固障時,整個網絡都不能連通。維護這種 網絡是非常麻煩的。通常採用得比較多的方法就是“切香腸”法。把最後一個電阻取下來,接到第二臺電腦的網絡節點的末端,檢查兩條線是否能連通。連通後再把 電阻取下來到第三臺電腦的網絡節點的末端,連上第三臺電腦。這樣來依次檢查整個網絡的線路。
後來發展了星形網絡,也是現在局域網普遍採用的。有一臺交換機,每一臺電腦連接到交換機,任意一個節點網絡故障不會影響到其它節點,檢查起來就非常方便了。沒有單元測試的代碼就像是環狀網絡,而有測試的代碼就像星形網絡。
 其次,有可能我們第一次編寫的代碼是沒有問題的,但 是到後來需求改變而修改了其中某些類的代碼,把它發佈到了應用服務器去測試,所要修改的內容已經測試通過了。但是因爲某些類的修改導致了其它類不能正常的 工作。這種bug往往隱藏得非常深,因爲只要不觸動它,它就不會出現。可能會程序發佈到生產環境之後纔會被業務人員發現。如果每個類都有測試代碼,我們在 打包之前運行所有測試代碼,就可以很容易的發現因爲代碼修改帶來的連帶性錯誤。
 其三,在離bug產生越近,修正bug就越容易;在bug產生越遠,修正bug的代價就越昂貴。假設我們去集成一個星期(甚至更長時間)前編寫的代碼,當發現問題時,我們已經忘掉了很多重要的實現細節,所以修改變得困難重重。
編寫單元測試,並不會加重程序員的負擔,反而提高了程序員對程序的信心,大大的減少了重複打包,發佈,糾錯誤的時間。這些花費的時間遠遠要比編寫單元測試花費的時間多幾個數量級。編寫單元測試,可以讓你更容易和更放心的去修改代碼,增加功能從而加快了項目的開發進度。
爲什麼我們總是要主觀的去認爲編寫單元測試會延緩項目進度呢?與其痛苦的掙扎,還不如去嘗試一下好的實踐。
 
二、業務邏輯簡單,不值得編寫單元測試
  程序員是聰明的,程序員也總是自認爲是聰明的。認 爲一些業務邏輯比較簡單的類不必要編寫單元測試。我們必須承認,需求不斷變化,我們也必須要有勇氣去接受需求變化。編寫單元測試的另一個目的就是擁抱變 化,而不是拒絕變化。編寫單元測試就是提高了我們對程序的信心。在敏捷軟件開發中,代碼爲集體所有,項目組的任何一個人都可以去修改任何一個代碼文件。每 當我要去修改一個別人編寫的代碼時,我總是多麼的希望有程序的單元測試代碼,而往往都讓我非常的失望。一般我都得花費很大的力氣去猜想原作者的原始意圖。 也許你會說:“你可以去看需求文檔啊!你不會去看註釋嗎?”。但實際情況是,當需求文檔完成了它的使命後,開發人員就把它扔到了一邊了,文檔總是過期的。 沒有幾個項目組能夠使得需求,設計這些文檔與最新實現代碼保持一致。所以去看一個過期的文檔是沒有價值的。註釋也同樣,保持最新仍然是一個最大的問題,並 且註釋能夠提供的信息是非常有限的。所以我最需要的就是看測試代碼了。測試代碼最能反映出方法最新的功能契約。由代碼的編寫者去寫的單元測試要比由其它人 去編寫的單元測試要更完善,更準確。
很多問題恰恰就出在一些我們認爲簡單的代碼中。除非是像一個JavaBean的getter和setter方法,因爲這些方法可以通過IDE自動代碼生成,沒有必要爲它編寫測試。
在項目開發中,我們需要經常通過重構來優化代碼及改進我們的設計,當我們對代碼進行重構之後,怎麼能夠保證代碼仍然是正確的?那就是運行所有被修改的代碼的測試。如果測試通過,則說明我們的重構是正確。
我們不能迴避代碼的維護問題。代碼維護包括修正bug和增加功能。維護工作可能會 距離代碼編寫完成有很長一段時間。當需要修改一個bug而修改了代碼,或增加一個新的功能而修改了代碼時,又怎麼能夠保證修改後的代碼仍然是正確的和沒有 隱患的呢?也許你會說,發佈到應該服務器去測試就知道了。筆者曾經發生過因爲維護而導致了更嚴重問題發生的情況。一個系統在生產環境正常運行很長時間了。 某一天,業務人員要求修改某一個功能,筆者按業務的要求實現了要修改的功能,業務也測試了修改後的功能,然後發佈到了生產環境。程序下發兩個星期後,報了 一個非常嚴重的生產問題上來,以前能夠正常運行的功能突然有問題了,導致了大量的生產數據錯誤。這個問題是非常致命的,只能暫時停用系統。
最後我查明原因是,出錯的模塊與上次修改的代碼有關聯,上次修改時沒有同時去修改現在出錯的模塊。要是我能夠在修改代碼後,運行所有的測試類,測試就肯定會報告不通過。也就不會把隱藏有這麼嚴重錯誤的程序下發到生產環境去。
  我們看看沒有寫單元測試是怎麼進行集成的。如果某些結果與我們所期望的不一致時,我們可能會在程序中加上許多 print語句,然後通過控制檯來監視程序的運行過程。採用print語句並不能夠保證我們的程序的正確性。最好的情況是,它只能保證一條正確的路徑,不 能保證其它的分支。另外當太多的print語句的信息在控制檯上,也會讓我們看不到想看到的信息,控制檯的信息是有限的。在開發測試時,把調試信息打印在 控制檯還可以接受,但是在生產環境,如果還有調試信息出現在控制檯,那是絕對不可以接受的。我們經常會忘記把調試的print語句及時的刪除掉,從而影響 程序的性能。最關鍵的是,print語句不能保證程序的正確性,也不能爲你節省開發的時間。只會給你帶來負面的影響。
 
三、不知道怎麼編寫單元測試
  如果你相信單元測試的價值,那麼去學習如何編寫單元測試最終會讓你獲益的。
以java開發爲例,junit這樣的單元測試組件是非常易於學習和使用的。其它語言也有類似的單元測試組件。要相信這將是簡單和能爲你帶來價 值的。但是筆者見過許多程序員編寫的單元測試完全沒有起到它應有的作用。這也與不知道怎麼編寫單元測試有關。所以我們應該掌握一些編寫單元測試的基本原 則:
  應該爲什麼編寫測試:雖然我們說爲所有的代類都編寫單元測試,但是測試JavaBean的setter或getter方法無異於是自尋煩惱。編寫這樣的測試完全是浪費時間。而且還增加了維護的困難。
  學會使用斷言:斷言就是讓我們爲方法設置一個期望值。當方法執行結果與期望值不一致時,測試組件就會報告測試不 通過。我見過一些項目的單元測試不是使用斷言,而是自己編寫一個打印(println)工具類,可以詳細的在控制檯中打印出類的詳細成員信息,及集合的詳 細信息。在單元測試中使用這個打印工具類來打印輸出結果。這看起來好像非常不錯。但是不應該使用這種方式來編寫單元測試
使用打印工具類,需要程序員自已從控制檯去觀察程序的執行結果。當輸出信息非常多時,控制檯信息是無法向上翻屏的。所以不能夠給我們提供更多的信息。所以這種方法也不能用於自動化測試。
使用打印工具類,造成了一種假像,測試報告我們的測試總是成功的!如果使用斷言,當方法的執行結果與我們設置的期望值不一致時,則會詳細的報告測試失敗的情況。
使用打印工具來代替斷言,造成測試的不充分,只會寫出一個低測試覆蓋率的測試。我們需要一個充分的測試。
  最大化測試覆蓋率:我們除了測試一個正確的路徑外,我們還需要測試方法的每一個分支邏輯。需要編寫儘可能多的測試程序邏輯的測試。寫一個充分的測試。
  避免重複的測試代碼:測試類也是非常重要的,與應用代碼一樣。測試類包含的重複代碼越多,測試類自身出現的錯誤也會越多。而我們需要做的編碼工作也就越多。
  不要依賴於測試方法的執行順序:使用Junit來進行單元測試,它不能保證測試方法按照我們的意圖的順序來執 行。當一個測試類有多個測試方法時,我們不能讓一個測試方法必須在某一個測試之後執行才能成功。Junit不能爲我們做這樣的保證,我們不能依賴於測試方 法的執行順序。
  針對接口測試:我們有“針對接口編程”的oo設計原則。同樣對於測試,我們也需要針對接口測試。也就是說在編寫單元測試時,測試對象總是使用接口,而不是使用具體類。
 
四、項目沒有要求,所以不編寫
  的確在很多項目中,團隊並沒有要求程序員爲每一個類編寫單元測試。反而會要求我們編寫很多複雜的文檔。作爲程序員我們需要明白:程序員是編寫單元測試的最大受益者。
這不是項目經理的事,也不是QA的事,而是程序員自身的事。因爲單元測試是程序代碼的一部份。單元測試是最好的,最有價值的文檔,它應該與代碼一起交付給客戶。
   單元測試代碼不是官僚,死板的文檔。它是生動的,是程序員最有用的文檔。單元測試能夠提高程序員對程序的信 心,能夠使用養成良好的設計原則:“針對接口編程,而不是具體類”。因爲要進行單元測試,所以我們需要讓類獨立於其依賴對象(使用Mock或stub)進 行測試。這就迫使我們養成了良好的編程習慣。
  單元測試是改進我們設計的保證。做爲一個優秀的程序員,是會經常優化代碼和設計,所以經常的進行重構。一個優秀的程序員絕對不能容忍異味代碼。而單元測試就是我們進行重構的信心保證。
  單元測試是一個日常開發活動,它貫穿於項目的整個生命週期。做一個負責任的程序員總是爲自己的代碼的質量負責的。是否經常改進你的設計,是否讓別人很輕鬆的使用和修改你的代碼。
爲所有類編寫單元測試應該是一個程序員應具有的素質。項目有沒有要求,不應當成爲不編寫單元測試的理由。
五、爲什麼越在項目的後期,單元測試就越難以進行下去?
  在很多項目的初期,項目中的大部分程序員都能夠自覺的去編寫單元測試。隨着項目的進展,任務的加重,離交付時間 越來越近,不能按時完成項目的風險越來越大,單元測試就往往成爲犧牲品了。項目經理因爲進度的壓力也不重視了,程序員也因爲編碼的壓力和無人看官而不再爲 代碼編寫單元測試了。
  筆者所有親歷的項目都着像這麼糟糕的情況發生。越是在項目的後期,能堅持編寫單元測試的程序就在整個項目組中不 會超過15%。爲了追趕進度,絕大多數程序員都把沒有經過任何測試的代碼提交到版本服務器,項目經理也不再追問,照單全收。這樣做的結果就是在後期,集成 花費的時間越來越多,幾個技術骨幹人員只得日夜加班進行系統集成。好不容易集成完了之後,下發給測試人員測試時,bug的報告成數量級的增長。程序員就日 以繼夜的修改bug.還有非常多的bug被隱藏更深,一直潛伏到生產環境去。項目中,越來越多的人對項目失去信心,每一個人都在抱怨,數不清的bug,修 正了一個bug,更多的bug報告上來。
每天都在修改bug,但是每天又會報告上更多的bug。於是開始有人想逃離了,有人請假,也有人離職。當項目總算結束時,每一個的內心都清楚,項目太爛了,還有很多的錯誤還沒被測試出來,趕快逃離這個項目組吧!一半的人病倒了,或對項目的維護失去了信心。
爲什麼會這樣?有沒有宣導測試的重要性呢?
在項目初期應該進行宣導單元測試的重要性。
有沒有做過相關的培訓工作?在項目啓動時,需要進行一些相關的培訓,教授團隊成員最基本的編寫單元測試的技巧。
有沒有做過相應的風險防範?越是工作資歷越深的程序員,就越會拒絕編寫單元測試,他們總是有太多的理由來拒絕編寫單元測試。這些頑固的老程序員 往往負責着核心的代碼的編寫。我們知道20-80定律吧。80%的錯誤是發生在20%的代碼之中的,往往最嚴重的錯誤就發生在那些老鳥們的代碼中。有沒有 在事先就做好風險防範,說服他們編寫單元測試。
有沒有做好測試相關的基礎工作。有沒有針對不同類型的程序編寫測試基類,讓編寫測試變成一項非常簡單的工作。有一些代碼是依賴於特定的環境,如 EJB訪問,JNDI訪問,web應用程序依賴Servlet API等。測試這些程序是非常困難的。應該編寫一些測試基類和測試stub,讓這些程序可以脫離於特定環境就像普通程序一樣進行單元測試。讓普通程序員輕 松的編寫測試代碼進行程序測試。
可以實行日構建和測試覆蓋率檢查,沒有通過測試的代碼絕不允許放到版本服務器。檢查測試的覆蓋率。
 
  在現代軟件開發過程中,測試不再作爲一個獨立的生命週期。單元測試成爲與編寫代碼同步進行的開發活動。單元測試能夠提高程序員對程序的信心,保證程序的質量,加快軟件開發速度,使程序易於維護。不管測試先行還是測試後行,沒有單元測試那是絕對不行的。
  弱者爲失敗找理由,強者爲成功找方法!今天你單元測試了嗎?


PS, 置田大先生親臨上海宣佈要玩TDD鳥
 發佈時間:2008-11-27 11:58:00 | 閱讀:142 | 評論:0 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章