重新思考單元測試

  • 2019 年 12 月 31 日
  • 筆記

摘要: 單元測試應該是程式設計師的必備技能,而真正的編程高手應該善於把握單元測試的粒度。

前一篇部落格,我提及到了最近在對後端Node.js服務進行程式碼重構,將Promise替換成Async/Await。這是一件痛並快樂著的事。

當任務完成50%之後,我發現,與其說是重構,更準確的說法或許是重寫。一方面,換用Async/Await本身就意味著需要修改每個非同步函數,而後端絕大多數函數都是非同步的;另一方面,作為一個有著強迫症的完美主義者,我寫了大量單元測試對程式碼進行了一系列優化,同時修復了一些BUG,並且實現了一個新功能。

這裡的關鍵詞是單元測試,那麼問題來了,重構程式碼就得了,寫什麼單元測試啊?這不是沒事找事么,要知道單元測試似乎比功能程式碼更難寫。

這是一個很有意思的話題。

什麼是單元測試

《玩轉Node.js單元測試》中,我是這樣定義單元測試的:

所謂單元測試,就是對某個函數或者API進行正確性驗證。

這樣的定義非常通俗易懂,但並不是很準確,嚴格來說應該是錯誤的。因為對API測試時,會涉及到多個函數,很多時候還會依賴於資料庫、快取以及第三方服務等外部資源。因此,API測試應該屬於集成測試而非單元測試

根據《JavaScript有這幾種測試分類》集成測試單元測試應該是這樣區分的:

單元測試指的是測試小的程式碼塊,通常指的是獨立測試單個函數。如果某個測試依賴於一些外部資源,比如網路或者資料庫,那它就不是單元測試。

集成測試就是測試應用中不同模組如何集成,如何一起工作,這和它的名字一致。集成測試與單元測試相似,但是它們也有很大的不同:單元測試是測試每個獨立的模組,而集成測試恰好相反。比如,當測試需要訪問資料庫的程式碼時,單元測試不會真的去訪問資料庫,而集成測試則會。

因此,對於單元測試,更加準確的理解應該是對單個函數進行獨立測試

但是,在實際操作中,測試單個函數時,很難保證所謂的獨立測試。一些函數難免依賴於其他函數、資料庫、函數以及第三方服務等外部資源,這個我們很難避免,甚至有時恰恰需要驗證這些外部資源。比如,驗證寫入資料庫或者快取的數據是否符合預期;驗證資料庫或者快取中的數據對函數行為的影響是否符合預期。

在我看來,對單個函數進行非獨立的測試,不妨也可以視作「單元測試」。簡單地說,本文所討論的單元測試,就是對單個函數進行測試

重構與單元測試

新功能的增加,程式碼複雜性的提高,優化程式碼的需要,或新技術的出現都會導致重構程式碼的需求。在沒有寫單元測試的情況下,對程式碼進行大規模修改,是一件不敢想像的事情,因為寫錯的概率實在太大了。

我一直在鼓勵大家寫單元測試,然而,有時難免偷懶。當我打算重構程式碼的時候,發現寫的單元測試其實是不夠的,這就比較尷尬了:(

那我到底是直接改程式碼;還是先寫單元測試,然後再改程式碼呢?這是一個艱難的決定,因為前者很難保證正確性,後者貌似需要耗費大量時間。

有一種智慧叫做「摸著石頭過河」:我嘗試在修改函數程式碼之前,補寫一些單元測試。這個過程並沒有想像中那麼痛苦,也許是因為做決定本身其實比做事情更痛苦,或者是因為我比較喜歡敲程式碼。

於是,我就可以開始大刀闊斧地進行重構了:換用Async/Await;優化程式碼組織;優化程式性能;寫新功能…忙得不亦樂乎。

如果沒寫單元測試,我敢怎麼做嗎?當然不敢!出錯了還得我來改啊。

如果沒寫單元測試,我會改得那麼快嗎?當然不會!大概每改一個函數都會想半天,改完然後祈禱它不會出錯;修改某個函數並不是一蹴而就的事情,如果每次修改都去磨嘰半天,大概我現在還在敲程式碼而不是在寫部落格。

正是因為有了單元測試做保證,改起來才會得心應手,效率更高。這樣,既可以保證正確性,又可以節省時間。想像中單元測試會浪費不少時間,事實上似乎並非如此。

Fundebug是全棧JavaScript錯誤監控平台,支援各種前端和後端框架,可以幫助您第一時間發現BUG!

單元測試的好處

也許大多數人沒有我這麼喜歡折騰,不會一直去重構程式碼,這種情況下,難道就不用寫單元測試啦?

我想答案應該是否定的。因為單元測試有很多顯而易見的好處:

  • 驗證程式碼的正確性
  • 驗證邊界條件
  • 避免BUG復現
  • 避免修改程式碼時出錯
  • 避免其他團隊成員修改程式碼時出錯
  • 便於自動化測試與部署

另外,單元測試能夠提供另一個思考程式碼的角度,這對於編寫高品質的程式碼是很有好處的。

本文聊的單元測試是針對每一個函數的,那麼,你在寫單元測試的時候,就會去考慮合理地拆分與合併函數。因為函數的功能區分不清楚的話,是不太好寫單元測試的。

敲程式碼的時候,我們考慮的是函數實現,不管三七二十一,寫好了就大功告成了。寫測試的時候,我們跳出了函數,從輸入輸出的角度去思考函數的功能,這時候,你就會去想,這個函數真的需要嗎?這個函數的功能是不是可以簡化一下?這個函數考慮的情況似乎不夠全面吧?這些思考,可以幫助我們寫出更好的程式碼。

單元測試的粒度

如果你是編程高手,似乎可以少寫一些單元測試。王垠大神在《測試的道理》中是這樣說的:

在我心目中,程式碼本身的地位大大的高於測試。我不忽視測試,但我不會本末倒置,過分強調測試,我並不推崇測試驅動開發(TDD)。我知道該測試什麼,不該測試什麼,什麼時候該寫測試,什麼時候不該寫,什麼時候應該推遲測試,什麼時候完全不需要測試。因為這個原因,再加上高強的編程能力,我多次完成別人認為在短時間不可能完成的任務,並且製造出品質非常高的程式碼。

那麼問題來了,你是高手嗎?根據二八原理,大部分開發者並非高手。在下自認為編程水平還不錯,也選擇盡量寫單元測試。

假設你是高手,那你能保證你的團隊都是高手嗎?根據二八原理,一個團隊裡面只有少數人是高手。如果你不寫足夠的單元測試,他們亂改你的程式碼,是會出事情的。

所以說,還是得盡量寫單元測試,無論你是不是高手。

當然,你也不能沒完沒了地寫單元測試,否則就本末倒置了。

另外,單元測試寫得越多,其邊際收益是在不斷降低,是得不償失的。神奇的二八原理告訴我們,20%的測試可以覆蓋80%的問題;而剩下20%的問題,你需要寫80%的單元測試。換句話說,單元測試並不能消除所有問題。因此,對生產程式碼進行實時錯誤監控是非常有必要的,這也是我們Fundebug努力在做的事情。

《單元測試要做多細?》中,耗子哥告訴我們:

UT的粒度是多少,這個不重要,重要的是你會不會自己思考你的軟體應該怎麼做,怎麼測試。

這是每一個程式設計師都應該認真思考的問題,沒有所謂的標準答案。從小接受中庸之道唯物主義辯證法熏陶的我們,應該可以在實踐當中思考合適的測試粒度。當你學會了思考,你才能成為真正的高手。

參考

版權聲明

轉載時請註明作者 Fundebug以及本文地址: https://blog.fundebug.com/2017/12/20/rethinking-unit-test/