儘管現在已經有了大量的軟體開發方法論及協助工具輔助協助Team Dev提高軟體品質,防止、檢測bug,但是一些很簡單實用的手段依然是提升軟體品質必須的手段,比如單元測試,比如Code review。
單元測試是一種很基本的軟體品質保證方法,隨著敏捷開發、持續整合的流行,一個高品質軟體,如果沒有單元測試是無法想象的。比如Java,C#都有非常成熟的單元測試架構,Google也開源了他們的C++單元測試架構以及mock架構。
這篇文章將探討幾個方面的問題,什麼是單元測試?單元測試為何如此重要?什麼時候進行單元測試?如何進行單元測試?單元測試要注意避免什麼問題?
什麼是單元測試?其實單元測試的本質就是assert,比如c語言就有內建的assert函數,在不滿足某個條件的時候返回錯誤碼,而MFC內建的ASSERT和VERIFY就更強大。如果你已經使用assert函數,其實你已經進入了單元測試的初級階段,稍微學習一下就可以掌握單元測試方法了。對於C++編程,可以使用google的googletest架構做單元測試,這種unittest架構的好處是整合了單元測試常見的需求,避免了重複開發。
常見的開發工作單位大致為兩種:維護legacy code或者偶爾加入一些小的feature,另外就是搭建一個新軟體。這兩種任務都不可避免的要反覆修改某個feature代碼,或者根據測試人員或使用者的反饋修改bug。我們如何保證修改以後的代碼沒有引入新的問題?如果你說你用人品擔保,那我服了。對於一個正規流程來說,應該有一種正式的手段來確保修改一個bug沒有引入兩個三個新的bug,或沒有導致以前正確的功能出錯,這就是單元測試的重要性,有了足夠的單元測試,你就可以理直氣壯的說新代碼沒有問題。
單元測試另一個重要性是協助你理清設計。對於反對單元測試的一個常見借口是,我們的應用太複雜了,沒法寫單元測試。不是應用複雜,其實是軟體設計有問題,導致沒法測試,可測試性也是軟體很重要的一個本質特性。如果設計中保證了某個函數某個介面只完成單一責任,沒有過多的耦合依賴,那麼測試其實是很簡單的事情。
單元測試另一個優點是可以整合到持續整合過程中,或者通過指令碼簡單快速反覆運行,不需要手動幹預,這對於提高開發效率而言非常重要。
什麼時候我們應該寫單元測試?是軟體代碼都寫完了,實際啟動並執行時候再寫嗎?單元測試其實應該在設計階段就開始寫,單元測試完成以後再寫實際功能部分代碼。設計應該是基於介面設計,單元測試也應該基於介面測試,另外針對某些複雜的內部邏輯,也應該有比較多的單元測試保證覆蓋率。對於某些核心部分,單元測試的代碼甚至應該超過實際代碼。而且要注意的是,應該將單元測試部分的代碼與工作代碼等同看待,一樣要做版本管理放入ClearCase或者SVN,而且單元測試部分的代碼也要review,保證測試代碼也是正確的。個人感覺單元測試(包括部分整合測試代碼)在整個代碼實現部分要佔30%到40%的任務量,這樣才比較正常。引入單元測試會在前期導致一些延遲,這是無法避免的,相應的會大大減少後期的維護工作,這是我自己的親身體會。
那麼該如何寫單元測試呢?單元測試能不能測GUI點擊輸入?首先要明確的一點是,單元測試不會替代其它測試手段,單元測試只是白盒測試的一種。單元測試是由開發人員編寫測試,保證正確完成某個邏輯功能的一種測試方法。而且單元測試不應該涉及到其它外部依賴或者其它的模組,比如GUI點擊、網路通訊、資料庫通訊或者需要安裝某個第三方軟體等等,這就需要開發人員做好設計,盡量把可能有耦合依賴的部分提取隔離,在整合測試或者其它測試的時候再檢查。
我們用一個簡單的例子解釋一下。某個GUI介面,當按下一個按鈕,它要變成另一種顏色,功能完成以後恢複原狀,或者是一個控制項允許使用者輸入,輸入完成以後校正,根據校正的結果儲存或者提醒使用者出現問題。這些都是比較常見的流程。這些流程顯然不是原子的(atom),涉及到model、View、control各個方面,某些程式員往往在CXXXDialog這樣的類裡面實現所有這些功能,還感覺封裝的非常好,“這不是物件導向封裝了嗎?我把它們都封裝到類裡面了啊?!”
我們就拿輸入校正來說明一下如何分解這個MVC過程。第一步,使用者輸入,點擊OK。這部分顯然是View和Control方面的,這部分可由tester方面做檢查測試,開發人員需要保證功能實現完整,簡單運行正確即可。第二步,檢查輸入資料,進行邏輯運算。這部分顯然是比較複雜的邏輯,涉及到Model和Control,一般不涉及到介面顯示,輸入部分就是資料,輸出部分就是檢查的結果。顯然,這部分應該做單元測試。第三步,如果出錯,反饋給使用者出錯的結果。這一部分基本上也是以介面顯示為主,是需要tester測試的部分。第四步,資料正確,儲存使用者的輸入。這部分涉及到資料運算,也是可以進行單元測試的,涉及到資料庫的部分,可能需要做整合測試。
從前面的分析可以看出,涉及到GUI介面的測試一樣可以有單元測試,需要開發人員做更多的工作,抽象邏輯計算部分代碼。這不容易實現,但是值得去做。
當進入後期開發階段,當使用者或者測試人員發現問題,開發人員就應該把這些問題轉化為測試案例,這樣既保證了修改後的代碼沒有導致其他bug重新出現,也是對代碼邏輯的一種很好的覆蓋。針對某些需要依賴其他模組的功能,我們可以mock介面,也可以編寫實現模組間的整合測試。另外,開發人員還可以進一步定製自己需要的單元測試架構功能,比如我就針對現在工作的項目,設計了一個靈活的添加測試案例的方案,測試案例用類似ini或者xml格式編寫,單元測試程式讀取測試案例進行測試。這樣引入一個新的測試案例就非常容易方便,不需要修改編譯代碼。當然這種設計也是針對我們項目的輸出主要為COM介面而定製的,更像是一種整合測試。
前面就是一些泛泛之談,沒有涉及到實際技術方法,只是鼓吹了單元測試的優點。希望各位程式員或技術領導能更加重視單元測試,在工作中使用單元測試,讓它真正成為日常工作的工具來保證軟體的高品質開發。