從開源項目看 Python 單元測試
我覺得以前在我開發程式的時候,除了文檔,可能單元測試是另外一個讓我希望別人都寫,但是自己又一點都不想寫的東西。但是,隨著開發程式的增多,以及自己對 Bug 的修改的增多,我發現,UT 在很大程度上是對我有利的,雖然帶來的結果就是可能我的 Dev 時間會增加 20-40% 左右,但是,相比較於一段時間之後突然冒出來一個 Bug,讓你摸不著頭腦;或者說突然一個接一個的 Bug 在你轉測試之後提過來,寫 UT 的幸福感和自豪感明顯是更高的。
就我目前而言,我認為寫單元測試有這麼幾個好處:
- 協助減小代碼的耦合度,這樣你才能更容易得編寫 UT
- 理清代碼的模組依賴,這樣你才能在 UT 中知道哪些東西要 Mock,哪些東西要 Stub
- 保持介面的乾淨和明朗,UT 就是針對介面編程,Input 和 Ouput 都需要明確
- UT 是一個自詮釋的文檔,別人可以通過你的 UT 來學習你的介面使用方式
這裡我需要提出一點的就是,一般而言,我很難做到 Test First,也就是所謂的先完成 UT 代碼的編寫,然後再寫實現代碼。當然,這不是說不行,就以目前的經曆來說,這做法欠妥,一個很重要的原因是項目周期的把控,如果你 UT First,萬一後面你時間不夠實現 Logical Code 了怎麼辦?光有 UT 並不能讓你的整個 Project 跑起來。所以,一般來說,我經曆的大部分項目都是先 Run 起來,然後再通過 UT 保證目前的功能是正常的,並且可以保證在以後的維護和更新中,功能的正確性不會被破壞。
測試方法
學過完善的軟體工程體系的同學都知道,軟體測試是軟體工程中非常重要的一環,甚至於可以說對於一個 Project,測試人員的參與度比 Developer 的高多了,我剛畢業那會,測試人員的參與度可以說是貫穿了全流程,從需求的提出到驗收發布,這整個流程都有測試人員的參與,而 Developer,可能參與到"轉測試"環境就差不多完了,所以測試工程也是一項非常複雜的學科。
測試覆蓋
因為測試非常複雜,所以也是有很多方法論和實踐的。就拿 UT 來說,對於代碼我們可以有幾個不同的測試角度。例如覆蓋角度來說,我們就有語句覆蓋,分支覆蓋,條件覆蓋,路徑覆蓋和迴圈覆蓋;測試內容來說,我們又會分模組測試,資料結構測試,路徑測試,錯誤處理測試和邊界測試等等。對於這麼多測試,其實我發現大部分開源項目都沒有很嚴格得遵守這些理論,因為可能說隨便一條理論在實踐中都能讓人抓狂。
其實在我見過的幾個流行的開源項目中,基本上都是以語句覆蓋為目標進行的,並且並不能達到 100%,所以更多得是以主要功能是正常的為目標進行 UT 測試的。以下是部分開源項目的測試結果:

除此之外,對於 UT 的增加是在 issue 的基礎上建立的,也就說當有使用者提了一個 issue 之後,Maintener 覺得這個 issue 是個問題,並且會影響到我這個項目,那麼就會開發開發相應的 patch fix 它,並且補上 UT,這種情況也是比較常見,這樣的話,漸漸地 UT 的覆蓋率也就慢慢上去了。
測試方法
在測試中,我們的代碼可能會有很多依賴,例如模組依賴,組件依賴等等,為瞭解決這些依賴,我們總要有一些方法來處理,這裡就有兩項經常使用的技術:Stub 和 Mock。
我以前喜歡說講一個對象 Mock 掉,意思就是講一個對象用自訂的類比類替換掉,從而讓我們可以自訂類的行為和輸出,但是,我發現這其實在測試中是 Stub,所謂的 Stub 就是類比測試代碼調用的模組和組件,從而自訂被調用後的行為和輸出;而相比之下,Mock 的功能是驗證模板或者組件有沒有被調用,很常見的例子就是郵件發送服務有沒有被調用,有沒有輸出日誌內容等等。關於 Stub 和 Mock 更多的內容介紹我推薦 Martin Folwer 的這篇文章:Mocks Aren't Stubs。
測試載入器
在 Python 中,自身就帶了類 XUnit 的 unittest 架構,使用也很簡單,例如下面就是一個很簡單的測試案例:

其實使用起來已經很簡單了,但是 Python 的小夥伴還是嫌他又囉嗦又慢,所以你會發現 pytest 這個庫很受歡迎。
pytest
pytest 作為一個單元測試架構,使用方法有多種,既可以和 python 內建的 unittest 類似,又可以很簡單得就一個函數來寫 UT;不僅開發效率會更高,而且執行效率也可以更高,其他優點就不囉嗦介紹了,官網裡面都羅列了:pytest。
至於有多簡單方便,你將下面這段代碼儲存到 test_sample.py
檔案中,然後在對應的目錄路徑下執行 pytest
命令

執行之後你應該會發現:

對,你會發現,就這麼簡單得執行起來了。但是,很多同學還是不滿於此,因為很多 Python 項目不僅僅適應於一個版本的 Python,所以就會有多一個 Python 版本的測試(Python 的又一坑,2.6/2.7/3.x/3.5 不相容)。
tox
為了滿足一個 Python 項目可以在多個 Python 版本中可以正常運行,很多人會使用 tox 進行不同環境下的相容性測試,所以 tox 的功能就是環境管理和測試回合。關於 tox 的更多功能使用和細則可以參考一下 tox 官網:Tox。
tox 一般都會有一個 tox.ini 檔案,例如一個簡單的例子:

然後執行 tox
命令列工具就可以了,它就會找你機器上的各種環境,然後測試起來,最後的結果就有點類似於:

小結
單元測試是一種習慣,也是一種責任。通過單元測試,我們可以告訴別人My Code是 Work 的,同時也給別人一種信任感,可以讓別人相信你寫的代碼。同時,編寫單元測試也是一項比較繁瑣的事情,我們要處理依賴,考慮 Test Case,但是,這些過程都可以協助我們更好得思考我們的項目和軟體,從而讓軟體的結構和代碼的品質提升一個台階。