Python 工匠Python 工匠

這是《Python 工匠|案例、技巧與開發實戰》筆記的第 2 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。

如第一篇所言,這是一本關於「Clean Code in Python」的書。

這二篇,我將整理書中第 13 章「有關單元測試的建議」的內容(以及我的看法)。我覺得真的寫得太好了,值得你了解。

話不多說,直接開始。

系列:Python 工匠

  1. 如何寫好 Python 註解——《Python 工匠》筆記
  2. 為什麼你「應該」寫單元測試——《Python 工匠》筆記
  3. 精通「變數」的宣告與使用——《Python 工匠》筆記

作者一開始就點了出關於「單元測試」的一個奇特現象

雖然好像人人都認為單元測試很有用,但在實際工作中,有完善單元測試的專案仍然是個稀奇的東西。

這一點也不誇張,甚至可以說是公開的秘密

單元測試,大家都說重要,但真的有在寫的團隊、專案、公司,卻不如想像中多。往往都是靠 QA 把關——把整合測試當作單元測試在測😎(我就待過這樣的公司)

很多公司都是在口頭上強調單元測試的重要性,但實際的執行力度卻大打折扣,就像是那些年初設定的新年決心,說的比做的好聽。

不寫單元測試的「理由」

姑且不說公司如何如何,其實就連開發者自己,也常常逃避寫單元測試。

書中舉出了實務上,開發人員不想寫單元測試的 3 個常見理由——以及這些「理由」為什麼不成立的理由:

  1. 時程緊迫沒時間寫測試」:寫單元測試看上去要多花費時間,但其實會在未來節約你的時間
  2. 模組複雜沒辦法寫測試」:也許這正代表了你的程式設計有問題,需要調整。
  3. 模組簡單不需要測試」:是否應該寫單元測試,和模組簡單或複雜沒有任何關係

第一個理由無疑是我們最常聽到的說法,但我們都知道,它往往只是一個藉口——就算真的有時間,說這種話的人也不會寫測試的啦!

拋開這些似是而非,各種不寫測試的牽強藉口,作者提出了 5 個,寫單元測試前值得先有的重要理解

接下來,讓我們一一說明。


一、寫單元測試不是浪費時間

寫單元測試可以節約你「整體」的開發時間。這個說法,我相信大部分的人應該都能夠接受——無論有沒有寫過測試。

那具體節約了哪些時間呢?主要有兩種:

  1. 專案更新功能後,舊的程式碼被新功能邏輯影響而產生 bug,為了 debug 所耗費的時間!
  2. 重構所需要的時間。

第一種情況,可謂天天都在發生——加了新東西,怎麼舊的就壞了!

不少人計算自己開發時間的方式,是只算「自己寫程式、 完成新功能」的時間,至於後續的 debug(更別說自己寫的爛扣造成後續無數次 debug 的時間),這些都不算!

QA 的日常

你可能聽過下列對白:

RD:「新功能我做完了,你測試一下吧!」

QA:「(經過一番測試)這 API 回傳不太對耶?還有這個參數好像少了一個?」

RD:「怎麼可能?」

好一句「怎麼可能」。

此時,QA 心中想的應不是「測一下」,而是「測幹譙(台語)」。

作者相信,你因為沒寫單元測試而耗費去處理後續問題的時間,絕對遠遠超過你寫單元測試的時間——我完全認同。這也許是「扁鵲梗:軟體工程師版」可以得到大量共鳴的原因。

當然,我也理解,寫單元測試通常不算是一件很有趣的事。但話說回來,當「有趣」和「重要」不可兼得的時候,我們也只能選擇後者。

誰叫我們是稱職的軟體工程師呢?

重構的勇氣

有單元測試第二個重大好處,就是它給你重構的勇氣。

假設你要對某個模組做大規模的重構,那麼,這個模組是否有單元測試,對應的重構難度天差地別。對於沒有任何單元測試的模組來說,重構是地獄難度

在這種環境下,每當你調整任何程式,都必須仔細找到模組的每一個被引用處,小心翼翼地手動測試每一個場景。稍有不慎,重構就會引入新 bug,好心辦壞事。

簡言之,對於複雜模組的重構,沒有單元測試是不可能的,這已經不是「有沒有時間」的問題了。


二、不要總想著「補」測試

「先幫我 review 下剛提交的這個 PR,功能已經全實現好了。單元測試我等等補上來!」

這樣說法的背後,透露著一種思維:單元測試是「」的,完全是「附屬」地位。所以可以事後再「」。

單元測試被當成了一種驗證正確性的事後工具,對開發功能程式沒有任何影響,因此,人們總是可以在完成開發後再補上測試。

但作者不這麼看:

但事實是,單元測試不光能驗證程式的正確性,還能極大地幫助你改進程式設計。但這種幫助有一個前提,那就是你必須在寫程式的同時寫單元測試。

當開發功能與寫測試同步進行時,你會來回切換自己的角色,分別作為程式的設計者和使用者,不斷從程式裡找出問題,調整設計。經過多次調整與打磨後,你的程式會變得更好、更具擴展性。

好吧!我承認,我自己也沒有很好地做到這點。

雖然很少提出要「補」測試,但我的開發還是處於「先寫功能再寫測試」的傳統習慣——尤其是專案的早期,API 還沒有完全底定的時候。

但我也相信,帶著「測試思維」來寫程式,絕對能夠讓程式碼的品質更上一階,這點我並不懷疑。

而要帶著測試思維寫程式,最簡單的方法,就是一邊寫程式一邊寫測試

我應該用 TDD 嗎?

說到這裡,你應該很容易聯想到 TDD

本書第 412 頁,作者寫了一個小專欄,專門討論他對 TDD 的看法。一言以蔽之就是:不一定要完全按照 TDD 的流程寫程式,但 TDD 的思維與習慣,值得你培養!

有興趣的讀者,可以自行參考書中內容。


三、難測試的程式就是爛程式

如果你認同了前述「一、二」的核心看法,那這個道理應該是自然而然的。

那些難寫測試的程式,本身很可能就有問題!我們看書中的一個例子:

當模組相依了一個全域物件時,寫單元測試就會變得很難。全域物件的基本特徵決定了它在記憶體中永遠只會存在一份。而在寫單元測試時,為了驗證程式在不同場景下的行為,我們需要用到多份不同的全域物件。這時,全域物件的唯一性就會成為寫測試最大的阻礙。

這類的例子真的多不勝數。大部分的時候,我們遇到這類情況,往往就是去「更改測試的邏輯」讓測試變得「剛好能夠通過」。

而帶來的結果往往是,測試的有效性降低!因為這個測試邏輯與方法和被測試的程式碼已經太過耦合了,可說是為了它「量身訂做」的測試。

如此一來,只要未來程式碼稍有變動,該測試也很可能就過不了。

現在我們知道,有時候不妨多想一下:「為什麼這麼難測試?」,並試著去重構原來的程式碼——而不是顧著修改測試函式本身。

因此,每當你發現很難為程式寫測試時,就應該意識到程式設計可能存在問題,需要努力調整設計,讓程式變得更容易測試。


四、像應用程式一樣對待測試程式

即使有在寫測試,這些測試程式碼,往往也被當作「二等公民」對待。

書中舉出了,把測試視為二等公民,因而「另眼看待(看輕)」它們的三個特徵

  1. 很能夠容忍測試程式碼大量重複
  2. 很能夠容忍測試程式執行上的「不效率」
  3. 鮮少重構測試程式碼。

其中潛藏並透露出的心態,用一句話來講就是:「測試的程式碼終究只是一個附屬品,所以程式碼品質差不多就好了,不要要求太多!」

作者當然不認同這樣的心態,並建議你:

像應用程式一樣對待測試程式。

和前述第二點一樣,我肯定認同這樣的看法,雖然還無法做到 100 分。

不過 80 分絕對是有的!

所以我在 code review 時,對於 pytest 的 fixture 設計,以及 fixtures 在測試函式中的引入與使用方式,會非常仔細審查。

你知道,測試寫的爛,也是一種「業障」。

總之,千萬別小看了測試程式碼。


五、避免教條主義

我個人覺得這段非常非常精彩,哈哈哈!

說起來很奇怪,在單元測試領域有非常多的理論與說法。人們總是樂於發表種對單元測試的見解,在文章、演講以及與同事的交談中,你常常能聽到這些話:

  • 「只有 TDD 才是寫單元測試的正確方式,其他都不行!」
  • 「TDD 已死,測試萬歲!」
  • 「單元測試應該純粹,任何相依都應該被 mock 掉!」
  • 「mock 是一種垃圾技術,mock 越多,表示程式越爛!」
  • 「只有專案測試覆蓋率達到 100%,才算是合格!」
  • ……

看到書中的這段,我真的會笑死XDDD——因為這個「怪現象」竟是如此的真實

從上述例子來看,這些立論不僅極端,而且往往還會互相矛盾

哪怕還沒有開始寫測試之前,我就已經看過不少這類言論。說真的,這些言論——或者說「信仰」——恐怕或多或少增加了想要入門測試的人,在心理上的門檻

好像你不把測試做到 100 分、盡善盡美,就乾脆不要寫測試了——我覺得這不是一種健康的姿態。

針對這現象,作者認為:

這些觀點各自都有許多狂熱的追隨者,但我有個建議:你應該了解這些理論,越多越好,但是千萬不要陷入教條主義

因為在現實世界裡,每個人參與的專案千差萬別,別人的理論不一定適用於你,如果盲目遵從,反而會給自己增加麻煩。

不必要的心理負擔

而我覺得,知曉有這個現象的存在,主要是為了「降低不必要的心理負擔」,進而認真看待自己已經寫好的測試,並對此感到欣慰。(但不自滿)

這很重要。

以我自己為例,目前工作上專案的測試覆蓋率大概在 60-75% 之間。而我清楚,想要從 75% 繼續再往上提升的話,必須要做很多「細節」工作。

比如為每一個自定義的 API 錯誤寫測試,確認錯誤訊息符合期待。(我們會適時地寫這些測試,但不是每一個都如此)

其中的投入與產出,不僅不成比例(投入多、效益有限),而且還可能造成開發者對於「寫單元測試」這件事感到厭煩。

所以並沒有嚴格要求覆蓋率要達到 100%——我想這也不太現實。

正確不等於適合

然而,每次看到文章或教學中的「100% 主義」,我還是多少會擔心:「難道我們做的真的還不夠好嗎?」——現在看來,大可不必。

這並不是說它們沒有道理。這些理論在某方面都是對的,但不一定適合你的專案。

單元測試領域的理論確實很多,這剛好說明了一件事,那就是要做好單元測試真的很難。要更好地實踐單元測試,你要做的第一件事就是拋棄教條主義,腳踏實地,不斷尋求最合適當前專案的測試方案,這樣才能最大地享受單元測試的好處。


但我還是不禁好奇!為什麼「測試」這領域,就是有這麼多狂熱信徒

這個議題,我們直接看看 ChatGPT 怎麼說吧!

狂熱信徒與他們的產地

以下內容整理自 ChatGPT(我劃了一些重點,以粗體字表示),我覺得分析得挺不錯,供有興趣的讀者參考。

這問題真是直擊核心,「測試」領域確實滋生了許多信仰狂熱者,原因有幾個方面:

  1. 人性的追尋絕對:在軟體開發這樣一個充滿不確定性的領域裡,人們往往尋求某種「萬靈丹」,希望找到一個可以解決所有問題的方法。當他們相信某種測試方法論能帶來開發上的救贖時,就很容易變成信仰狂熱者。
  2. 成功案例的誤導:經常有人會分享他們使用特定測試方法論取得成功的案例,但很少有人會分享失敗的經驗。這種片面的資訊會誤導人們認為某種方法是萬能的,從而盲目追隨。
  3. 社群與身分認同:一旦某人投入到特定的測試方法論中,他們很可能會加入相關的社群。在這樣的社群裡,方法論被當作教條,社群成員的身分認同也與這些信仰緊密相連。為了保護自己的身分認同,人們往往會變得更加狂熱。
  4. 技術宗教戰爭的延伸:軟體開發領域充斥著各種技術宗教戰爭,無論是程式語言、開發框架,還是測試方法論。這些戰爭反映了人們對自我選擇的辯護,以及對他人選擇的攻擊。
  5. 缺乏批判性思考:最後,也是最讓人頭疼的原因之一。很多人接受新的方法論時,缺乏批判性思考。他們不去評估這些方法論是否適合自己的專案,不考慮成本和收益,也不尋找可能的替代方案。

這些原因綜合在一起,就創造了一個完美的環境,讓「測試」領域裡的信仰狂熱者茁壯成長。