重新思考單元測試
摘要: 單元測試應該是程式設計師的必備技能,而真正的程式設計高手應該善於把握單元測試的粒度。
在前一篇部落格,我提及到了最近在對後端Node.js服務進行程式碼重構,將Promise替換成Async/Await。這是一件痛並快樂著的事。
當任務完成50%之後,我發現,與其說是重構,更準確的說法或許是重寫。一方面,換用Async/Await本身就意味著需要修改每個非同步函式,而後端絕大多數函式都是非同步的;另一方面,作為一個有著強迫症的完美主義者,我寫了大量單元測試對程式碼進行了一系列優化,同時修復了一些BUG,並且實現了一個新功能。
這裡的關鍵詞是單元測試,那麼問題來了,重構程式碼就得了,寫什麼單元測試啊?這不是沒事找事麼,要知道單元測試似乎比功能程式碼更難寫。
這是一個很有意思的話題。
什麼是單元測試
所謂單元測試,就是對某個函式或者API進行正確性驗證。
這樣的定義非常通俗易懂,但並不是很準確,嚴格來說應該是錯誤的。因為對API測試時,會涉及到多個函式,很多時候還會依賴於資料庫、快取以及第三方服務等外部資源。因此,API測試應該屬於整合測試而非單元測試。
單元測試指的是測試小的程式碼塊,通常指的是獨立測試單個函式。如果某個測試依賴於一些外部資源,比如網路或者資料庫,那它就不是單元測試。
整合測試就是測試應用中不同模組如何整合,如何一起工作,這和它的名字一致。整合測試與單元測試相似,但是它們也有很大的不同:單元測試是測試每個獨立的模組,而整合測試恰好相反。比如,當測試需要訪問資料庫的程式碼時,單元測試不會真的去訪問資料庫,而整合測試則會。
因此,對於單元測試,更加準確的理解應該是對單個函式進行獨立測試。
但是,在實際操作中,測試單個函式時,很難保證所謂的獨立測試。一些函式難免依賴於其他函式、資料庫、函式以及第三方服務等外部資源,這個我們很難避免,甚至有時恰恰需要驗證這些外部資源。比如,驗證寫入資料庫或者快取的資料是否符合預期;驗證資料庫或者快取中的資料對函式行為的影響是否符合預期。
在我看來,對單個函式進行非獨立的測試,不妨也可以視作“單元測試”。簡單地說,本文所討論的單元測試,就是對單個函式進行測試。
重構與單元測試
新功能的增加,程式碼複雜性的提高,優化程式碼的需要,或新技術的出現都會導致重構程式碼的需求。在沒有寫單元測試的情況下,對程式碼進行大規模修改,是一件不敢想象的事情,因為寫錯的概率實在太大了。
我一直在鼓勵大家寫單元測試,然而,有時難免偷懶。當我打算重構程式碼的時候,發現寫的單元測試其實是不夠的,這就比較尷尬了:(
那我到底是直接改程式碼;還是先寫單元測試,然後再改程式碼呢?這是一個艱難的決定,因為前者很難保證正確性,後者貌似需要耗費大量時間。
有一種智慧叫做“摸著石頭過河”:我嘗試在修改函式程式碼之前,補寫一些單元測試。這個過程並沒有想象中那麼痛苦,也許是因為做決定本身其實比做事情更痛苦,或者是因為我比較喜歡敲程式碼。
於是,我就可以開始大刀闊斧地進行重構了:換用Async/Await;優化程式碼組織;優化程式效能;寫新功能…忙得不亦樂乎。
如果沒寫單元測試,我敢怎麼做嗎?當然不敢!出錯了還得我來改啊。
如果沒寫單元測試,我會改得那麼快嗎?當然不會!大概每改一個函式都會想半天,改完然後祈禱它不會出錯;修改某個函式並不是一蹴而就的事情,如果每次修改都去磨嘰半天,大概我現在還在敲程式碼而不是在寫部落格。
正是因為有了單元測試做保證,改起來才會得心應手,效率更高。這樣,既可以保證正確性,又可以節省時間。想象中單元測試會浪費不少時間,事實上似乎並非如此。
Fundebug是全棧JavaScript錯誤監控平臺,支援各種前端和後端框架,可以幫助您第一時間發現BUG!
單元測試的好處
也許大多數人沒有我這麼喜歡折騰,不會一直去重構程式碼,這種情況下,難道就不用寫單元測試啦?
我想答案應該是否定的。因為單元測試有很多顯而易見的好處:
- 驗證程式碼的正確性
- 驗證邊界條件
- 避免BUG復現
- 避免修改程式碼時出錯
- 避免其他團隊成員修改程式碼時出錯
- 便於自動化測試與部署
另外,單元測試能夠提供另一個思考程式碼的角度,這對於編寫高質量的程式碼是很有好處的。
本文聊的單元測試是針對每一個函式的,那麼,你在寫單元測試的時候,就會去考慮合理地拆分與合併函式。因為函式的功能區分不清楚的話,是不太好寫單元測試的。
敲程式碼的時候,我們考慮的是函式實現,不管三七二十一,寫好了就大功告成了。寫測試的時候,我們跳出了函式,從輸入輸出的角度去思考函式的功能,這時候,你就會去想,這個函式真的需要嗎?這個函式的功能是不是可以簡化一下?這個函式考慮的情況似乎不夠全面吧?這些思考,可以幫助我們寫出更好的程式碼。
單元測試的粒度
如果你是程式設計高手,似乎可以少寫一些單元測試。王垠大神在《測試的道理》中是這樣說的:
在我心目中,程式碼本身的地位大大的高於測試。我不忽視測試,但我不會本末倒置,過分強調測試,我並不推崇測試驅動開發(TDD)。我知道該測試什麼,不該測試什麼,什麼時候該寫測試,什麼時候不該寫,什麼時候應該推遲測試,什麼時候完全不需要測試。因為這個原因,再加上高強的程式設計能力,我多次完成別人認為在短時間不可能完成的任務,並且製造出質量非常高的程式碼。
那麼問題來了,你是高手嗎?根據二八原理,大部分開發者並非高手。在下自認為程式設計水平還不錯,也選擇儘量寫單元測試。
假設你是高手,那你能保證你的團隊都是高手嗎?根據二八原理,一個團隊裡面只有少數人是高手。如果你不寫足夠的單元測試,他們亂改你的程式碼,是會出事情的。
所以說,還是得儘量寫單元測試,無論你是不是高手。
當然,你也不能沒完沒了地寫單元測試,否則就本末倒置了。
另外,單元測試寫得越多,其邊際收益是在不斷降低,是得不償失的。神奇的二八原理告訴我們,20%的測試可以覆蓋80%的問題;而剩下20%的問題,你需要寫80%的單元測試。換句話說,單元測試並不能消除所有問題。因此,對生產程式碼進行實時錯誤監控是非常有必要的,這也是我們Fundebug努力在做的事情。
UT的粒度是多少,這個不重要,重要的是你會不會自己思考你的軟體應該怎麼做,怎麼測試。
這是每一個程式設計師都應該認真思考的問題,沒有所謂的標準答案。從小接受中庸之道和唯物主義辯證法薰陶的我們,應該可以在實踐當中思考合適的測試粒度。當你學會了思考,你才能成為真正的高手。
參考
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了8億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!
您的使用者遇到BUG了嗎?
體驗Demo 免費使用