為什麼你「應該」寫單元測試——《Python 工匠》筆記
Python 工匠
這是《Python 工匠|案例、技巧與開發實戰》筆記的第 2 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。
如第一篇所言,這是一本關於「Clean Code in Python」的書。
這二篇,我將整理書中第 13 章「有關單元測試的建議」的內容(以及我的看法)。我覺得真的寫得太好了,值得你了解。
話不多說,直接開始。
系列:Python 工匠
- 如何寫好 Python 註解——《Python 工匠》筆記
- 為什麼你「應該」寫單元測試——《Python 工匠》筆記
- 精通「變數」的宣告與使用——《Python 工匠》筆記
作者一開始就點了出關於「單元測試」的一個奇特現象:
雖然好像人人都認為單元測試很有用,但在實際工作中,有完善單元測試的專案仍然是個稀奇的東西。
這一點也不誇張,甚至可以說是公開的秘密。
單元測試,大家都說重要,但真的有在寫的團隊、專案、公司,卻不如想像中多。往往都是靠 QA 把關——把整合測試當作單元測試在測😎(我就待過這樣的公司)
很多公司都是在口頭上強調單元測試的重要性,但實際的執行力度卻大打折扣,就像是那些年初設定的新年決心,說的比做的好聽。
不寫單元測試的「理由」
姑且不說公司如何如何,其實就連開發者自己,也常常逃避寫單元測試。
書中舉出了實務上,開發人員不想寫單元測試的 3 個常見理由——以及這些「理由」為什麼不成立的理由:
- 「時程緊迫沒時間寫測試」:寫單元測試看上去要多花費時間,但其實會在未來節約你的時間。
- 「模組複雜沒辦法寫測試」:也許這正代表了你的程式設計有問題,需要調整。
- 「模組簡單不需要測試」:是否應該寫單元測試,和模組簡單或複雜沒有任何關係。
第一個理由無疑是我們最常聽到的說法,但我們都知道,它往往只是一個藉口——就算真的有時間,說這種話的人也不會寫測試的啦!
拋開這些似是而非,各種不寫測試的牽強藉口,作者提出了 5 個,寫單元測試前值得先有的重要理解。
接下來,讓我們一一說明。
一、寫單元測試不是浪費時間
寫單元測試可以節約你「整體」的開發時間。這個說法,我相信大部分的人應該都能夠接受——無論有沒有寫過測試。
那具體節約了哪些時間呢?主要有兩種:
- 專案更新功能後,舊的程式碼被新功能邏輯影響而產生 bug,為了 debug 所耗費的時間!
- 重構所需要的時間。
第一種情況,可謂天天都在發生——加了新東西,怎麼舊的就壞了!
不少人計算自己開發時間的方式,是只算「自己寫程式、 完成新功能」的時間,至於後續的 debug(更別說自己寫的爛扣造成後續無數次 debug 的時間),這些都不算!
QA 的日常
你可能聽過下列對白:
RD:「新功能我做完了,你測試一下吧!」
QA:「(經過一番測試)這 API 回傳不太對耶?還有這個參數好像少了一個?」
RD:「怎麼可能?」
好一句「怎麼可能」。
此時,QA 心中想的應不是「測一下」,而是「測幹譙(台語)」。
作者相信,你因為沒寫單元測試而耗費去處理後續問題的時間,絕對遠遠超過你寫單元測試的時間——我完全認同。這也許是「扁鵲梗:軟體工程師版」可以得到大量共鳴的原因。
當然,我也理解,寫單元測試通常不算是一件很有趣的事。但話說回來,當「有趣」和「重要」不可兼得的時候,我們也只能選擇後者。
誰叫我們是稱職的軟體工程師呢?
重構的勇氣
有單元測試第二個重大好處,就是它給你重構的勇氣。
假設你要對某個模組做大規模的重構,那麼,這個模組是否有單元測試,對應的重構難度天差地別。對於沒有任何單元測試的模組來說,重構是地獄難度。
在這種環境下,每當你調整任何程式,都必須仔細找到模組的每一個被引用處,小心翼翼地手動測試每一個場景。稍有不慎,重構就會引入新 bug,好心辦壞事。
簡言之,對於複雜模組的重構,沒有單元測試是不可能的,這已經不是「有沒有時間」的問題了。
二、不要總想著「補」測試
「先幫我 review 下剛提交的這個 PR,功能已經全實現好了。單元測試我等等補上來!」
這樣說法的背後,透露著一種思維:單元測試是「多」的,完全是「附屬」地位。所以可以事後再「補」。
單元測試被當成了一種驗證正確性的事後工具,對開發功能程式沒有任何影響,因此,人們總是可以在完成開發後再補上測試。
但作者不這麼看:
但事實是,單元測試不光能驗證程式的正確性,還能極大地幫助你改進程式設計。但這種幫助有一個前提,那就是你必須在寫程式的同時寫單元測試。
當開發功能與寫測試同步進行時,你會來回切換自己的角色,分別作為程式的設計者和使用者,不斷從程式裡找出問題,調整設計。經過多次調整與打磨後,你的程式會變得更好、更具擴展性。
好吧!我承認,我自己也沒有很好地做到這點。
雖然很少提出要「補」測試,但我的開發還是處於「先寫功能再寫測試」的傳統習慣——尤其是專案的早期,API 還沒有完全底定的時候。
但我也相信,帶著「測試思維」來寫程式,絕對能夠讓程式碼的品質更上一階,這點我並不懷疑。
而要帶著測試思維寫程式,最簡單的方法,就是一邊寫程式一邊寫測試!
我應該用 TDD 嗎?
說到這裡,你應該很容易聯想到 TDD。
本書第 412 頁,作者寫了一個小專欄,專門討論他對 TDD 的看法。一言以蔽之就是:不一定要完全按照 TDD 的流程寫程式,但 TDD 的思維與習慣,值得你培養!
有興趣的讀者,可以自行參考書中內容。
三、難測試的程式就是爛程式
如果你認同了前述「一、二」的核心看法,那這個道理應該是自然而然的。
那些難寫測試的程式,本身很可能就有問題!我們看書中的一個例子:
當模組相依了一個全域物件時,寫單元測試就會變得很難。全域物件的基本特徵決定了它在記憶體中永遠只會存在一份。而在寫單元測試時,為了驗證程式在不同場景下的行為,我們需要用到多份不同的全域物件。這時,全域物件的唯一性就會成為寫測試最大的阻礙。
這類的例子真的多不勝數。大部分的時候,我們遇到這類情況,往往就是去「更改測試的邏輯」讓測試變得「剛好能夠通過」。
而帶來的結果往往是,測試的有效性降低!因為這個測試邏輯與方法和被測試的程式碼已經太過耦合了,可說是為了它「量身訂做」的測試。
如此一來,只要未來程式碼稍有變動,該測試也很可能就過不了。
現在我們知道,有時候不妨多想一下:「為什麼這麼難測試?」,並試著去重構原來的程式碼——而不是顧著修改測試函式本身。
因此,每當你發現很難為程式寫測試時,就應該意識到程式設計可能存在問題,需要努力調整設計,讓程式變得更容易測試。
四、像應用程式一樣對待測試程式
即使有在寫測試,這些測試程式碼,往往也被當作「二等公民」對待。
書中舉出了,把測試視為二等公民,因而「另眼看待(看輕)」它們的三個特徵:
- 很能夠容忍測試程式碼大量重複。
- 很能夠容忍測試程式執行上的「不效率」。
- 鮮少重構測試程式碼。
其中潛藏並透露出的心態,用一句話來講就是:「測試的程式碼終究只是一個附屬品,所以程式碼品質差不多就好了,不要要求太多!」
作者當然不認同這樣的心態,並建議你:
像應用程式一樣對待測試程式。
和前述第二點一樣,我肯定認同這樣的看法,雖然還無法做到 100 分。
不過 80 分絕對是有的!
所以我在 code review 時,對於 pytest 的 fixture 設計,以及 fixtures 在測試函式中的引入與使用方式,會非常仔細審查。
你知道,測試寫的爛,也是一種「業障」。
總之,千萬別小看了測試程式碼。
五、避免教條主義
我個人覺得這段非常非常精彩,哈哈哈!
說起來很奇怪,在單元測試領域有非常多的理論與說法。人們總是樂於發表種對單元測試的見解,在文章、演講以及與同事的交談中,你常常能聽到這些話:
- 「只有 TDD 才是寫單元測試的正確方式,其他都不行!」
- 「TDD 已死,測試萬歲!」
- 「單元測試應該純粹,任何相依都應該被 mock 掉!」
- 「mock 是一種垃圾技術,mock 越多,表示程式越爛!」
- 「只有專案測試覆蓋率達到 100%,才算是合格!」
- ……
看到書中的這段,我真的會笑死XDDD——因為這個「怪現象」竟是如此的真實。
從上述例子來看,這些立論不僅極端,而且往往還會互相矛盾。
哪怕還沒有開始寫測試之前,我就已經看過不少這類言論。說真的,這些言論——或者說「信仰」——恐怕或多或少增加了想要入門測試的人,在心理上的門檻。
好像你不把測試做到 100 分、盡善盡美,就乾脆不要寫測試了——我覺得這不是一種健康的姿態。
針對這現象,作者認為:
這些觀點各自都有許多狂熱的追隨者,但我有個建議:你應該了解這些理論,越多越好,但是千萬不要陷入教條主義。
因為在現實世界裡,每個人參與的專案千差萬別,別人的理論不一定適用於你,如果盲目遵從,反而會給自己增加麻煩。
不必要的心理負擔
而我覺得,知曉有這個現象的存在,主要是為了「降低不必要的心理負擔」,進而認真看待自己已經寫好的測試,並對此感到欣慰。(但不自滿)
這很重要。
以我自己為例,目前工作上專案的測試覆蓋率大概在 60-75% 之間。而我清楚,想要從 75% 繼續再往上提升的話,必須要做很多「細節」工作。
比如為每一個自定義的 API 錯誤寫測試,確認錯誤訊息符合期待。(我們會適時地寫這些測試,但不是每一個都如此)
其中的投入與產出,不僅不成比例(投入多、效益有限),而且還可能造成開發者對於「寫單元測試」這件事感到厭煩。
所以並沒有嚴格要求覆蓋率要達到 100%——我想這也不太現實。
正確不等於適合
然而,每次看到文章或教學中的「100% 主義」,我還是多少會擔心:「難道我們做的真的還不夠好嗎?」——現在看來,大可不必。
這並不是說它們沒有道理。這些理論在某方面都是對的,但不一定適合你的專案。
單元測試領域的理論確實很多,這剛好說明了一件事,那就是要做好單元測試真的很難。要更好地實踐單元測試,你要做的第一件事就是拋棄教條主義,腳踏實地,不斷尋求最合適當前專案的測試方案,這樣才能最大地享受單元測試的好處。
但我還是不禁好奇!為什麼「測試」這領域,就是有這麼多狂熱信徒?
這個議題,我們直接看看 ChatGPT 怎麼說吧!
狂熱信徒與他們的產地
以下內容整理自 ChatGPT(我劃了一些重點,以粗體字表示),我覺得分析得挺不錯,供有興趣的讀者參考。
這問題真是直擊核心,「測試」領域確實滋生了許多信仰狂熱者,原因有幾個方面:
- 人性的追尋絕對:在軟體開發這樣一個充滿不確定性的領域裡,人們往往尋求某種「萬靈丹」,希望找到一個可以解決所有問題的方法。當他們相信某種測試方法論能帶來開發上的救贖時,就很容易變成信仰狂熱者。
- 成功案例的誤導:經常有人會分享他們使用特定測試方法論取得成功的案例,但很少有人會分享失敗的經驗。這種片面的資訊會誤導人們認為某種方法是萬能的,從而盲目追隨。
- 社群與身分認同:一旦某人投入到特定的測試方法論中,他們很可能會加入相關的社群。在這樣的社群裡,方法論被當作教條,社群成員的身分認同也與這些信仰緊密相連。為了保護自己的身分認同,人們往往會變得更加狂熱。
- 技術宗教戰爭的延伸:軟體開發領域充斥著各種技術宗教戰爭,無論是程式語言、開發框架,還是測試方法論。這些戰爭反映了人們對自我選擇的辯護,以及對他人選擇的攻擊。
- 缺乏批判性思考:最後,也是最讓人頭疼的原因之一。很多人接受新的方法論時,缺乏批判性思考。他們不去評估這些方法論是否適合自己的專案,不考慮成本和收益,也不尋找可能的替代方案。
這些原因綜合在一起,就創造了一個完美的環境,讓「測試」領域裡的信仰狂熱者茁壯成長。
相關文章