程式員為什麼不寫單元測試?

來源:互聯網
上載者:User

一、為了單元測試而寫單元測試

    最近筆者曾經做過一次“程式員在項目開發中編寫單元測試的情況”的調查。 
    調查結果顯示:
1. 幾乎沒有嚴格在項目中執行TDD(,TDD)。
2. 為大部份業務方法編寫單元測試,並保證方法測試通過,佔16.6%。
3. 偶爾編寫單元測試,一般情況下不寫單元測試,佔58.3%。
4. 為了應付項目檢查而寫單元測試,但並不保證方法是否測試通過, 佔8.3%。
5. 從來不編寫單元測試,佔16.6%。 
    雖然調查的結果有一定的片面性,但是佔58.3%比例的確高的驚人,同時,從來不編寫單元測試16.6%人層也基本反映國內程式員編寫單元測試的狀況,很少有程式員能夠比較認真地去編寫單元測試。那麼,到底又是什麼原因導致程式員不編寫單元的測試的?根據筆者參與的多個討論,主要有下面幾種原因使程式員不編寫單元測試:
1. 為了完成編碼任務,沒有足夠的時間編寫單元測試。編寫單元測試會導致不能按時完成編碼任務,延遲項目進度。
2. 單元測試的價值不高,完全是浪費時間。
3. 商務邏輯比較簡單,不值得編寫單元測試。
4. 不知道怎麼編寫單元測試。
5. 項目沒有要求,所以不編寫。
6. 在項目的前期還是盡量去編寫單元測試,但是越到項目的後期就越失控。 
    測試常常是程式員十分厭倦的一個項目活動。測試能夠為我們帶來什嗎?瞭解這些非常的重要,測試不可能保證一個程式是完全正確的,但是測試卻可以增強我們對程式完整的信心,測試可以讓我們相信程式做了我們期望它做的事情。測試能夠使我們儘早地發現程式的bug和不足。 
    一個bug被隱藏的時間越長,修複這個bug的代價就越大。在《快速軟體開發》一書中已引用了大量的研究資料指出:最後才修改一個bug的代價是在bug產生時修改它的代價的10倍。 
    在這裡,我們需要討論的重點是單元測試。單元測試是一個方法層級上的測試,單元測試也是最細粒度的測試。用於測試一個類的每一個方法都已經滿足了方法的功能要求。 
    在現代軟體開發過程中,不管是XP還是RUP都是十分重視單元測試。已經把單元測試作為貫穿整個開發週期的一項重要的開發活動。特別是在現代軟體開發過程中,有經常整合和漸近提交的方法論。由此,總結出了非常好的單元測試理論和實踐。

 

二、在編寫代碼之前先編寫單元測試,即測試先行
    單元測試是代碼的一部份,所有的代碼必須有單元測試,並使測試通過(像在Spring這些優秀的開源項目中在這方面做出了非常好的例子)。 
    在修改代碼之前先修改單元測試,並使它測試通過。 
    在編寫代碼之前先編寫單元測試,會帶來非常多的好處。 
    在編寫代碼之前先編寫單元測試,並不是編寫代碼之前需要一次性為所有的類都事先編寫單元測試,這需要有一個粒度的控制。最大的粒度應該控制在一個類層級上,最合適的粒度是控制在一個方法層級上。先為某一個方法編寫測試代碼,然後再為該方法編寫實現代碼,直到其測試通過後再為另一個方法編寫測試代碼,如此迴圈。單元測試在這裡已經是一個契約規範了,它規範了方法應該做什麼、實現什麼。測試代碼遠遠要比難以閱讀和不會及時更新的需求文檔更有價值得多。 
    測試先行,鼓勵對需求的理解。如果沒有理解需求,你是不可能寫出測試代碼的,當然你也不可能寫出好的實現代碼。 
    測試代碼與其它文檔相比會更有價值。當需求發生改變,實現代碼也相應改變。而往往需求文檔、設計文檔得不到及時更新。測試代碼相比那些到期的文檔更具有價值。 
    測試先行可以編寫出最大覆蓋率的測試代碼。如果在方法的實現代碼編寫完後再編寫測試代碼,這時開發人員總是編寫一個正確路徑的測試代碼。它已經很難全面的去分析其它分支邏輯。 
    如果我們採用測試先行,那麼就自動地完成了為所有的類都編寫測試。為所有的類都編寫測試會將為你帶來非常多的好處。 
    我們可以很好地使用自動化測試來測試所有的類,特別是採用日構建的系統。可以讓我們放心地為類或方法添加新的功能。我們可以很容易地修改測試代碼並驗證修改後的代碼是有用的代碼。可以讓我們放心地對代碼進行重構和進行設計最佳化。 
    重構和設計最佳化通常會關聯到多個類及多個方法。如果我們為所有的類都編寫了測試,我們就可以在重構代碼後很輕鬆地進行測試我們的修改是否正確。 
    為所有的類編寫測試,可以讓我們很容易地修改bug。當接到一個bug報告後,我們總是先修改測試代碼,然後修改實現代碼,使測試成功。這樣不會因為修改一個問題而造成新問題的產生。 
    良好的單元測試策略給我們增強了對程式的信心,減少了bug的產生及bug的潛伏期,降低修改bug的代價。 
    單元測試不會是項目開發週期的某一個生命週期,它貫穿於項目的整個生命週期,是一個非常重要的日常開發活動。

三、程式員緣何拒絕編寫單元測試?
    我們已經知道了單元測試是多麼重要的。為什麼程式員仍然不編寫單元測試呢?為什麼程式員總是有理由拒絕編寫單元測試呢?

 
1、 編寫單元測試,增加了工作負擔,會延緩項目進度? 

    這是筆者在多次討論和調查中見到程式員拒絕編寫單元測試的最多理由。“為了完成編碼任務,沒有足夠的時間編寫單元測試。編寫單元測試會導致不能按時完成編碼任務,延遲項目進度”。事實上真的是這樣的嗎? 
    軟體有著其特殊的生命週期,軟體開發也具有特殊性。 
    首先,我們需要提供給使用者的至少是一個能啟動並執行產品。絕對不能是一堆不能啟動並執行和充滿了“異味”的無作用程式碼。只有能夠啟動並執行,滿足客戶需求的代碼才是真正有用的代碼。這時,代碼就變成產品了。 
    很多程式員只注重編寫代碼的完成時間,而乎略了調試代碼,整合及修改和維護時間。 
    如果沒有單元測試,開發活動會是這樣情景。 
    以一個Web應用開發為例,流程大概如此:業務代碼編寫完成打包發布到伺服器進行功能測試發現問題修改代碼再打包……如此迴圈。 
    任何一個Web程式員對於這種開發情景都不會感到陌生。往往不斷的打包、發布、功能測試的時間是代碼編寫的10倍以上。通過整合系統來發現程式的bug,我們往往很難一下子準確的定位bug產生的地方。應用伺服器提供的錯誤資訊對於我們來說是非常有限的。 
    如果為每一個類都編寫單元測試並讓每一個方法測試通過,又會是怎麼樣的開發情景呢? 
    編寫測試代碼編寫業務代碼運行測試方法修改代碼讓測試通過所有的類都通過測試打包發布到伺服器進行功能測試發現bug修改測試代碼修改業務代碼測試通過再打包……如此迴圈。 
    從上面的過程顯而易見,我們需要花費更多的編碼時間。因為需要為每一個業務類編寫測試代碼。但是,它並不會導致我們總體需要花費更多的時間。我們只是可以非常輕鬆的在IDE環境中運行測試方法。 
    在代碼尚未打包發布之前我們就已經確保了業務代碼的正確性。當我們把所有通過測試的代碼整合到應用伺服器後,出現錯誤的機率要少得多。當整合測試後發現bug時,我們也總是先修改測試類別。保證在整合之前所有的類都經過測試通過。這樣,功能測試的時間就成數量級的減少,所以總的花費時間要比沒有單元測試要少得多。 
    另外,如果沒有單元測試,會經常出現一些低級的錯誤,如拼字錯誤、null 指標異常等。就因為一個小小的拼字錯誤而需要重新打包、發布一次。如果有單元測試,就可以避免這些低級的錯誤。 
    如果沒有單元測試,把代碼整合到應用伺服器後再發現錯誤時,我們往往更多的是憑藉自己的經驗來判斷問題出在哪裡。對於沒有經驗的程式員來說只能是撞運氣了。這就像是瞎子走路一樣,兩眼一摸黑。如果每個類都有單元測試,就無需要這麼痛苦了。 
    寫到這裡,使得我回想起當年做網路系統。當時的區域網路絡都是採用環狀網路,還沒有現在的交換器來組星形網路。環狀網路的傳輸網路採用同軸細纜線,網路中的所有節點都在一條主幹線上,網路的兩端都會加上一個電阻來形成一個環(如)。

    環狀網路的最大的缺點就是當任意一個節點有固障時,整個網路都不能連通。維護這種網路是非常麻煩的。通常採用得比較多的方法就是“切香腸”法。把最後一個電阻取下來,接到第二台電腦的網路節點的末端,檢查兩條線是否能連通。連通後再把電阻取下來到第三台電腦的網路節點的末端,連上第三台電腦。這樣來依次檢查整個網路的線路。 
    後來就發展了星形網路,也是現在區域網路普遍採用的。有一台交換器,每一台電腦串連到交換器,任意一個節點網路故障不會影響到其它節點,檢查起來就非常方便了。沒有單元測試的代碼就像是環狀網路,而有測試的代碼就像星形網路。 
    其次,有可能我們第一次編寫的代碼是沒有問題的,但是到後來需求改變而修改了其中某些類的代碼,把它發布到了應用伺服器去測試,所要修改的內容已經測試通過了。但是因為某些類的代碼的修改導致了其它類不能正常的工作。這種bug往往隱藏得非常深,因為只要不觸動它,它就不會出現。可能會程式發布到生產環境之後才會被業務人員發現。如果每個類都有測試代碼,我們在打包之前運行所有測試代碼,就可以很容易的發現因為代碼修改帶來的連帶性錯誤。 
    最後,在離bug產生越近,修正bug就越容易;在bug產生越遠,修正bug的代價就越昂貴。假設我們去整合一個星期(甚至更長時間)前編寫的代碼,當發現問題時,我們已經忘掉了很多重要的實現細節,所以修改變得困難重重。 
    編寫單元測試,並不會加重程式員的負擔,反而提高了程式員對程式的信心,大大的減少了重複打包、發布、錯誤修正誤的時間。這些花費的時間遠遠要比編寫單元測試花費的時候多幾個數量級。編寫單元測試,可以讓你更容易和更放心地去修改代碼,增加功能從而加快了項目的開發進度。 
    為什麼我們總是要主觀的去認為編寫單元測試會延緩項目進度呢?與其痛苦的掙紮,還不如去嘗試一下好的實踐。

 

2、商務邏輯簡單,不值得編寫單元測試
    程式員是聰明的,程式員也總是自認為是聰明的。認為一些商務邏輯比較簡單的類不必要編寫單元測試。我們必須承認,需求不斷變化,我們也必須要有勇氣去接受需求變化。編寫單元測試的另一個目的就是擁抱變化,而不是拒絕變化。編寫單元測試就是提高了我們對程式的信心。 
    在敏捷式軟體開發 (Agile Software Development)中,代碼為項目組所有成員所有,項目組的任何一個人都可以去修改任何一個代碼檔案。每當我要去修改一個別人編寫的代碼時,我總是多麼的希望有程式的單元測試代碼,往往都讓我非常的失望。一般我都得花費很大的力氣去猜想作者的原始意圖。也許你會說:“你可以去看需求文檔啊!你不會去看注釋嗎?”。但實際情況是,當需求文檔完成了它的使命後,開發人員就把它扔到了一邊了,文檔總是到期的。沒有幾個項目組能夠使得需求,設計這些文檔與最新實現代碼保持一致。所以去看一個到期的文檔是沒有價值的。注釋也同樣,保持最新仍然是一個最大的問題,並且注釋能夠提供的資訊是非常有限的。所以我最需要的就是看測試代碼了。測試代碼最能反映出方法最新的功能契約。由代碼的編寫者去寫的單元測試要比由其它人去編寫的單元測試要更完善、更準確。 
    很多問題恰恰就出在一些我們認為簡單的代碼中。除非是像一個JavaBean的getter和setter方法,因為這些方法可以通過IDE自動代碼產生,沒有必要為它編寫測試。 
    在項目開發中,我們需要經常通過重構來最佳化代碼及改進我們的設計,當我們對代碼進行重構之後,怎麼能夠保證代碼仍然是正常的?那就是運行所有被修改的代碼的測試。如果測試通過,則說明我們的重構是正確。 
    我們不能迴避代碼的維護問題。代碼維護包括修正bug和增加功能。維護工作可能會距離代碼編寫完成有很長一段時間。當需要修改一個bug而修改了代碼,或增加一個新的功能而修改了代碼時,又怎麼能夠保證修改後的代碼仍然是正確的和沒有隱患的呢? 
    也許你會說,發布到應該伺服器去測試就知道了。筆者曾經發生過因為維護而導致了更嚴重問題發生的情況。一個系統在生產環境正常運行很長時間了。某一天,業務人員要求修改某一個功能,筆者按業務的要求實現了要修改的功能,業務也測試了修改後的功能,然後發布到了生產環境。程式下發兩個星期後,報了一個非常嚴重的生產問題上來,以前能夠正常啟動並執行功能突然有問題了,導致了大量的生產資料錯誤。這個問題是非常致命的,只能暫時停用系統。 
    最後我查明原因是,出錯的模組與上次修改的代碼有關聯,上次修改時沒有同時去修改現在出錯的模組。要是我能夠在修改代碼後,運行所有的測試類別,測試就肯定會報告不通過。也就不會把隱藏有這麼嚴重錯誤的程式下發到生產環境去。 
    我們看看沒有寫單元測試是怎麼進行整合的。如果某些結果與我們所期望的不一致時,我們可能會在程式中加上許多print語句,然後通過控制台來監視程式的運行過程。採用print語句並不能夠保證我們的程式的正確性。最好的情況是,它只能保證一條正確的路徑,不能保證其它的分支。另外當太多的print語句的資訊在控制台上,也會讓我們看不到想看到的資訊——控制台的資訊是有限的。在開發測試時,把調試資訊列印在控制台還可以接受,但是在生產環境,如果還有調試資訊出現在控制台,那是絕對不可以接受的。我們經常會忘記把調試的print語句及時的刪除掉,從而影響程式的效能。最關鍵的是,print語句不能保證程式的正確性,也不能為你節省開發的時間。只會給你帶來負面的影響。

 

3、不知道怎麼編寫單元測試
    如果你相信單元測試的價值,那麼去學習如何編寫單元測試最終會讓你獲益的。 
    以Java開發為例,junit這樣的單元測試組件是非常易於學習和使用的。其它語言也有類似的單元測試組件。要相信這將是簡單和能為你帶來價值的。筆者見過許多程式員編寫單元測試,但是編寫的單元測試完全沒有起到它應有的作用,這也與不知道怎麼編寫單元測試有關。所以我們應該掌握一些編寫單元測試的基本原則: 
    •為什麼編寫測試:雖然我們說為所有的代類都編寫單元測試,但是測試JavaBean的setter或getter方法無異於是自尋煩惱。編寫這樣的測試完全是浪費時間,而且還增加了維護的困難。 
    •學會使用斷言:斷言就是讓我們為方法設定一個期望值。當方法執行結果與期望值不一致時,測試組件就會報告測試不通過。我見過一些項目的單元測試不是使用斷言,而是自己編寫一個列印(println)工具類,可以詳細的在控制台中列印出類的詳細成員資訊及集合的詳細資料。 
    在單元測試中使用這個列印工具類來列印輸出結果。這看起來好像非常不錯。但是不應該使用這種方式來編寫單元測試使用列印工具類,需要程式員自已從控制台去觀察程式的執行結果。當輸出資訊非常多時,控制台資訊是無法向上翻屏的。所以不能夠給我們提供更多的資訊。所以這種方法也不能用於自動化測試。 
    使用列印工具類造成了一種假像,測試報告我們的測試總是成功的!如果使用斷言,當方法的執行結果與我們設定的期望值不一致時,則會詳細的報告測試失敗的情況。 
    使用列印工具來代替斷言,造成測試的不充分,只會寫出一個低測試覆蓋率的測試。我們需要一個充分的測試。 
    •最大化測試覆蓋率:我們除了測試一個正確的路徑外,還需要測試方法的每一個分支邏輯。需要編寫儘可能多的測試程式碼的測試。寫一個充分的測試。 
    •避免重複的測試代碼:測試類別也是非常重要的,與應用代碼一樣。測試類別包含的重複代碼越多,測試類別自身出現的錯誤也會越多。而我們需要做的編碼工作也就越多。 
    • 不要依賴於測試方法的執行順序:使用Junit來進行單元測試,它不能保證測試方法按照我們的意圖的順序來執行。當一個測試類別有多個測試方法時,我們不能讓一個測試方法必須在某一個測試之後執行才能成功。Junit不能為我們做這樣的保證,我們不能依賴於測試方法的執行順序。 
    •針對介面測試:我們有“針對介面編程”的OO設計原則。同樣對於測試,我們也需要針對介面測試。也就是說在編寫單元測試時,測試對象總是使用介面,而不是使用具體類。

 

4、項目沒有要求,所以不編寫單元測試

    的確,在很多項目中團隊並沒有要求我們為每一個類編寫單元測試,反而會要求我們編寫很多複雜的文檔。作為程式員我們需要明白:程式員是編寫單元測試的最大受益者。 
    這不是專案經理的事,也不是QA的事,而是程式員自身的事,因為單元測試是程式碼的一部份。單元測試是最好的,最有價值的文檔,它應該與代碼一起交付給客戶。 
    單元測試代碼不是官僚、死板的文檔。它是生動的,是程式員最有用的文檔。單元測試能夠提高程式員對程式的信心,能夠使用養成良好的設計原則:“針對介面編程,而不是具體類”。因為要進行單元測試,所以我們需要讓類獨立於其依賴對象(使用Mock或stub)進行測試。這就迫使我們養成了良好的編程習慣。 
    單元測試是改進我們設計的保證。做為一個優秀的程式員,是會經常最佳化代碼和設計,所以經常的進行重構。一個優秀的程式員絕對不能容忍“異味”代碼,而單元測試就是我們進行重構的信心保證。 
    單元測試是一個日常開發活動,它貫穿於項目的整個生命週期。做一個負責任的程式員總是為自己的代碼的品質負責的。是否經常改進你的設計,是否讓別人很輕鬆的使用和修改你的代碼。 
    為所有類編寫單元測試應該是一個程式員應具有的素質。項目有沒有要求,不應當成為不編寫單元測試的理由。

 

5、為什麼越在項目的後期,單元測試就越難以進行下去?
    在很多項目的初期,項目中的大部分程式員都能夠自覺的去編寫單元測試。隨著項目的進展,任務的加重,離交付時間越來越近,不能按時完成項目的風險越來越大,單元測試就往往成為犧牲品了。專案經理因為進度的壓力也不重視了,程式員也因為編碼的壓力和無人看管而不再為代碼編寫單元測試了。 
    筆者所有親曆的項目都或多或少地有這麼糟糕的情況發生。越是在項目的後期,能堅持編寫單元測試的程式就在整個項目組中不會超過15%。 
    為了追趕進度,絕大多數程式員都把沒有經過任何測試的代碼提交到版本伺服器,專案經理也不再追問,照單全收。這樣做的結果就是在後期,整合花費的時間越來越多,幾個技術骨幹人員只得日夜加班進行系統整合。好不容易整合完了之後,下發給測試人員測試時,bug的報告成數量級的增長。程式員就日以繼夜的修改bug.還有非常多的bug被隱藏更深,一直潛伏到生產環境去。在項目中,越來越多的人對項目失去信心,每一個人都在抱怨,數不清的bug,修正了一個bug,更多的bug報告上來。 
    每天都在修改bug,但是每天又會報告上更多的bug。於是開始有人想逃離了,有人請假,也有人離職。當項目總算結束時,每一個人的內心都清楚,項目太爛了,還有很多的錯誤還沒被測試出來,趕快逃離這個項目組吧!一半的人病倒了,或對項目的維護失去了信心。 
    為什麼會這樣?有沒有宣導測試的重要性呢?在項目初期應該進行宣導單元測試的重要性。 
    有沒有做過相關的培訓工作?在項目啟動時,需要進行一些相關的培訓,教授團隊成員最基本的編寫單元測試的技巧。 
    有沒有做過相應的風險防範?越是工作資曆越深的程式員,就越會拒絕編寫單元測試,他們總是有太多的理由來拒絕編寫單元測試。這些“頑固”的老程式員往往負責著核心的代碼的編寫。我們知道“20-80定律”吧。80%的錯誤是發生在20%的代碼之中的,往往最嚴重的錯誤就發生在那些“老鳥”們的代碼中。有沒有在事先就做好風險防範,說服他們編寫單元測試。 
    有沒有做好測試相關的基礎工作。有沒有針對不同類型的程式編寫測試基類,讓編寫測試變成一項非常簡單的工作。有一些代碼是依賴於特定的環境,如EJB訪問、JNDI訪問、Web應用程式依賴Servlet API等,而測試這些程式是非常困難的。應該編寫一些測試基類和測試stub,讓這些程式可以脫離於特定環境就像普通程式一樣進行單元測試。讓普通程式員輕鬆的編寫測試代碼進行程式測試。 
    可以實行日構建和測試覆蓋率檢查,沒有通過測試的代碼絕不允許放到版本伺服器。檢查測試的覆蓋率。 
    在現代軟體開發過程中,測試不再作為一個獨立的生命週期。單元測試成為與編寫代碼同步進行的開發活動。單元測試能夠提高程式員對程式的信心,保證程式的品質,加快軟體開發速度,使程式易於維護。不管測試先行還是測試後行,沒有單元測試那是絕對不行的。 
    弱者其找理由,強者找方法!今天你單元測試了嗎?

原文地址

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.