寫測試代碼這種事情 ,以前只在網上和書上看到過, 自己從來沒有寫過。 每當看到那些世界頂級程序員編寫的技術書籍中出現“測試用例”“測試代碼”的字樣或者一些行業的鼎鼎大名的技術大牛們提及寫測試的重要性的時候,我的心里就會產生一種自己編的一定是假程的錯覺, 為什么我寫代碼就從來不用那玩意?
就拿開發一個MVC框架的Web應用程序設來說, 通常的做法就是新建一個控制器和一個模型, 把代碼要實現功能的業務邏輯寫在模型里面,控制器調用模型, 假如有外部參數則接收參數傳遞給模型, 假如業務邏輯過於復雜導致模型過於臃腫或邏輯不順暢, 則再進行梳理或提取,構建成一個新類,再由模型進行調用。 這一過程反復循環迭代, 直到功能開發完畢。 調試或者測試寫的代碼是否能得出想要的結果, 自然也是使用最簡單粗暴的方法, 在瀏覽器中運行程序, 定位到控制器, 控制器調用模型, 模型再調用其它所涉及到的類,拿到結果后再一步步返回, 瀏覽器是否顯示預期結果就意味着我們寫的程序是否正確。不管是我們要測試的功能模塊離控制器只有一個調用還是有十個調用,都遵循着這樣一個步驟, 因為這是最符合我們直覺和習慣的方式。 我也一直以這種方式在開發程序。
原本這也沒有什么問題,我們所寫的代碼邏輯是通過我們的大腦深思熟慮組織后產生的,通常情況下我們有這個把握可以確定代碼邏輯運行的正確性,就算出現意料之外的情況, 多點幾下瀏覽器的刷新按扭也能把問題找出來解決,因為我們對代碼的運行邏輯了然於胸,自信不會出什么叉子,一旦出現了叉子那就產生了所謂的程序BUG。
然而, 萬事總有例外, 導致我們以往的經驗失效。 就拿我最近碰到的一件事情來說,公司有一個項目因性能優化需要,對部分功能進行技術方案調整, 重寫了代碼。代碼量不大, 功能本身的代碼和其依賴的通用函數代碼加起來一共也就二三百行,但是隱含在背后的邏輯卻異常復雜,涉及到的數據表也有五張。我將這部分需要重寫的代碼重頭至尾仔仔細細讀了一遍, 勉強能理解每一個語句塊都干了些什么。 可能是我邏輯思維能力不過關, 也有可能是代碼太過於復雜 , 我沒有辦法將所有這些代碼的來龍去脈全盤了然於胸,也就沒有辦法從全局的角度去梳理代碼邏輯確定優化方案,我只能從局部的角度出發, 依樣畫葫蘆的按照舊方案重新實現一遍代碼的邏輯, 在實現的過程中如發現有優化的余地則進行局部優化,等到足夠熟悉全局邏輯后,再從宏觀的角度對代碼結構進行調整優化,這么做效率是低了點,卻是最保險的做法。
我照着舊代碼寫出一個個一模一樣的函數,卻沒有辦法確定這些函數的運行結果是否能得出預期的結果,鬼知道換一種語言實現以后, 函數吐出來的結果還是不是和之前的一樣,我可沒有jeff dean那樣牛逼,預判代碼的結果比編譯器還精准。本來這也不是什么大問題,把代碼跑一遍,當執行到這些函數的調用時自然就知道結果了。問題出在這之中某些函數和代碼的入口隔着七八個調用,而且其中某些調用因為依賴於某些if條件判斷結果而不是必然被調用到的,要構造出能使這些函數被調用到的if條件判斷分支走向的參數環境是一件異常繁瑣的事情,光想想就讓人覺得煩躁和氣餒。另一種方法就是把函數的調用代碼復制一份放到執行入口的開始位置,這樣代碼一運行就直接能調用的到了。 然而, 這種方法也會帶來問題,如
函數處於不同的類和包內,調用函數需要導入包和實例化類,而做這些事情對項目的本身沒有實際的意義
某幾個函數只在所在的類內被調用, 訪問修飾是private, 通過這種方法測試它的准確性還需要放開權限把訪問修飾聲明為public, 調試完畢后還得改回去, 操蛋
有多個分布在不同類之中的不同函數需要以類似的方式測試, 反復進行這些無意義且繁瑣的操作, 極度浪費時間,影響心情
代碼的執行入口總放着那么一坨被注釋掉的代碼,想拿掉又怕拿掉以后下次還要用, 內心掙扎的難受
因此, 想要解決這個問題, 上面的兩種方案都不可取, 柔腸百轉也想不出像樣的解決方案。 長輩們都說編程都是腦力勞力, 我之前不以為然, 但當碰到這些想破腦袋也找不到辦法的問題時就不得不承認, 編程的確是腦力勞力。我才20歲,外表卻有30歲可以看,我想也跟長期被這些問題困擾有一定的關系(我說的是10年前的自己)
我思前想后,檢索所有腦子中關於程序設計的資源, 才找出一個之前從來沒有嘗試過的方案, 引入單元測試。我這個人有一個優點, 在工作上碰到陌生的東西從來不會望而卻步,只要有用處, 都會去積極嘗試。對於單元測試,我雖然沒有掌握使用的方法, 但是網上查查資料, 看看教程, 我相信花不了多少功夫就能搞出來。 事實也的確如此, 只看了一篇資料,照着教程的步驟操作就把測試程序跑起來了。 我使用的是go語言, 按照go test的規則 ,被測試的代碼所在的文件名加上test后綴即可作為測試代碼所在的文件的命名,如下圖
測試函數的命名方式必須要以Test作為前綴, 如下圖
測試代碼編寫完成后, 在代碼所在的文件目錄下使用cmd運行go test命令,測試代碼就可被運行了
需要測試的函數在測試代碼中被直接調用, 省去了跟蹤龐雜代碼執行走向的麻煩,從復雜的業務邏輯中解放出來, 非常的清晰方便。
從表面上看, 寫測試代碼的好處就是方便測試函數的正確性, 然而, 隨着之后代碼的編寫, 我發現寫測試代碼所帶來的好處不止於此。當有了要為代碼編寫測試用例的前提條件后, 我在實現某個函數時就約束自己, 這個函數必須要方便編寫相應的測試代碼。有了這層約束以后, 我發現寫出來的代碼的質量要比不寫測試用例時高, 比如
函數的功能職責更加單一了,換言之, 函數的邏輯更穩定了, 不易產生變動, 因為我不想我辛苦編寫的測試代碼隨着函數的代碼的調整而付之一炬。
不會很隨意的把代碼亂放, 寫出來的代碼更加整潔,該提取函數時就建新函數, 該內聯函數時則刪除不必要的函數,在之前, 為了偷懶往往會對這些細節視而不見, 這會加速代碼的腐爛。
更早的發現BUG,很多時候, 程序的BUG都是在生產環境中由用戶發現,原因很簡單, 開發項目的速度和質量這對冤家之間程序員往往會選擇前者,此外, 程序員會毫無根據的信任自己寫的代碼,因此當向程序員反饋BUG時,他們都會保持懷疑的態度。很多時候, 程序員寫一個函數通常只給一個特定的輸入,運行后發現輸出如自己預期那樣后就默認這個函數是健康的, 事實上, 當給這個函數另外的輸入時, 函數吐出的結果就在預期范圍之外, 這便導致了BUG的產生, 個中原因便是對自己直覺盲目的信任, 認為自己的大腦就是一個人肉編譯器。 編寫測試可以很大程度上的杜絕這類問題
通常,我們會認為編寫測試是一件浪費時間的事情, 然后就是一邊向別人吹牛一邊則啪啪啪的打自己臉。 除此之此, 在開發項目時常常以邏輯不穩定隨時需要調整代碼為理由拒絕寫測試,然而, 當從相反的方向來考慮問題時會發現, 有了測試的約束后,我們會更加仔細和嚴謹去編寫每一個函數 ,逼迫自己更加深入的考慮問題而防止代碼走樣, 提高代碼質量、安全性以及穩定性, 這也是寫測試所帶來的至關重要的意義。