MOOSSin
選單

全部文章 · 開源工具

unittest 還是 pytest:測試框架什麼時候值得多裝一個套件

pytest 的 fixture、參數化和斷言訊息都比標準函式庫的 unittest 好用,這沒有爭議。有爭議的是「多一個相依」在你的專案裡代價多大。這篇講我在幾個專案裡各選一邊的理由,以及用 unittest 也能做到的分級執行。從「第一個相依最貴」這個觀念講起。

大一的時候我有一個很奇怪的習慣:宿舍書桌上什麼都不放。不是整潔,是我發現只要放了第一樣東西,第二樣、第三樣就會自動跟過來,然後一個學期後桌上會有一座山。零和一之間的距離,比一和一百之間遠很多。

後來寫程式的時候發現專案的相依也是這樣。Python 的測試框架兩個選擇:標準函式庫的 unittest,和 MIT 授權、幾乎成為業界預設的 pytest。我的幾個專案裡兩種都有,選哪一個不是看喜好,是看「多一個相依」在那個專案裡的代價。

pytest 好在哪

先說清楚,pytest 真的比較好用,這件事沒有爭議,我不打算假裝 unittest 一樣好。

fixture 用函式參數注入,你要一個暫存資料庫,就在測試函式的參數列寫 tmp_db,它會自動幫你建好、用完幫你收。@pytest.mark.parametrize 把一組輸入展開成多個測試,十組資料一個函式,失敗的時候告訴你是第幾組。斷言失敗時它會重寫 assert 顯示兩邊的實際值,不用自己寫 assertEqual(a, b, f"expected {b} got {a}") 這種東西。-k 挑名字、-x 遇錯即停、pytest.ini 集中設定。外掛生態很大,什麼都有人寫過。寫測試的體驗,pytest 明顯好,好到用過就很難回去。

unittest 好在哪

它在標準函式庫裡。這句話的份量取決於專案的狀態。

一個還沒有 requirements.txt、沒有套件管理器、沒有虛擬環境的專案,第一個相依會啟動一整套義務:鎖定檔、SBOM、第三方授權聲明、跨平台的雜湊。這些東西我在別篇講過,每一樣都不難,但加起來是一個下午,而且從此以後每次更新都要再來一次。為了跑幾個檔案存在與否的斷言就啟動這一套,成本和收益不成比例。unittest 加 pathlib、re、subprocess 已經夠寫很多檢查。

另一個好處是它不預設任何事。選了 pytest 就等於選了 Python 當專案語言、選了 pip 當套件來源,在那些決定還沒做出來之前,標準函式庫讓你保持中立。就像書桌上什麼都不放,你還可以決定它要拿來讀書還是拿來畫畫。

用 unittest 也能分級跑

pytest 沒有壟斷「聰明地跑測試」這件事。一個幾十行的執行器可以提供三個等級:fast 只跑最重要的幾個模組,affected 依變更的檔案挑相關的測試,full 探索所有 tests/test_*.py。在 affected 無法保證安全的情況下自動升級成 full,在容器裡跑時自動跳過只在容器外有意義的斷言。這些都用 unittest.TestLoader 和 unittest.TextTestRunner 就能做。

寫這個執行器大概花了一個晚上。之後每次改一行程式,我可以選擇跑三秒的 fast 還是兩分鐘的 full,這個選擇權比任何外掛都值錢。

小劇場:學長的建議

「你寫測試居然用 unittest?現在誰還用那個。」

「這個專案沒有任何相依。」

「裝一個 pytest 又不會怎樣。」

「裝一個 pytest 就會有 requirements、有 lock、有授權清單。這個專案現在什麼都沒有,我想讓它維持什麼都沒有。」

「你是不是有什麼潔癖。」

「有一點。但你等等看那個資料專案,我用 pytest,因為它本來就有四十個相依,多一個不痛不癢。」

「所以你不是反對 pytest。」

「我反對的是第一個相依,不是第二十個。」

我在哪裡選哪個

只有文件和靜態產物、不打算引進任何相依的專案:unittest,透過標準函式庫直譯器跑,連虛擬環境都不建。桌上什麼都不放。

有 FastAPI 這類正式相依、已經有鎖定檔和 SBOM 的網站專案:還是 unittest。理由是這幾個專案的測試大量依賴暫存 SQLite、假服務和自寫的 ASGI 用戶端,pytest 的 fixture 換成 setUp 和 addCleanup 也寫得出來,而多一個測試專用的相依會讓「執行期相依」和「開發相依」的清單分家,多一份要維護的東西。這裡桌上已經有東西了,但我不想再開第二個抽屜。

只在本機跑、來源模組超過一百個、每個都要對同樣一批情境跑一遍的資料專案:pytest。參數化在這裡省下的重複程式碼是真的,一百個模組乘以十個情境,用 unittest 寫會寫到手抽筋。那個專案本來就有幾十個相依,多一個 pytest 沒有改變什麼,桌上早就是一座山了,多放一本書沒差。

我的判斷標準

第一個相依最貴,第二十個相依幾乎免費。專案還在零相依的狀態時,守住它;已經有一票相依時,別為了純粹主義放棄好用的工具。

這個原則聽起來很矛盾,一邊說要守住零、一邊說有了就別客氣。但它其實只在講一件事:成本是有階梯的,跨過第一階的成本最高,之後每一階都便宜很多。知道自己站在哪一階,決定就不難。至於我的書桌,畢業前還是變成一座山了,原則這種東西,知道跟做到是兩回事。

這兩個框架從哪來

unittest 的祖先是 1990 年代 Kent Beck 為 Smalltalk 寫的 SUnit,後來他和 Erich Gamma 把同樣的想法搬到 Java 變成 JUnit,JUnit 紅了之後每個語言都出現一個「xUnit」,Python 的版本在 2001 年進了標準函式庫。這就是為什麼 unittest 長那個樣子:類別、setUp、tearDown、assertEqual,那是 Java 的味道,二十幾年前的設計,跟 Python 本身的風格有點格格不入,但它穩定得像一塊石頭。

pytest 是 2004 年 Holger Krekel 從一個叫 py 的函式庫裡長出來的,一開始只是他自己用的測試工具。它的核心想法很 Python:測試就是函式,斷言就是 assert,不需要繼承任何類別。fixture 用參數注入的設計是後來加的,那個設計讓它跟 unittest 拉開了距離。它還能直接跑 unittest 寫的測試,所以從 unittest 遷移到 pytest 幾乎沒有成本,這是它能變成事實標準的原因之一。

常見的三個誤會

第一個誤會是「unittest 不能用參數化」。可以,用 subTest,只是寫法沒有 pytest 漂亮。功能上的差距比大家以為的小,體驗上的差距比大家以為的大。

第二個誤會是「用 pytest 就會寫出好測試」。框架決定測試怎麼組織,不決定測試測什麼。壞的測試在兩個框架裡一樣壞,只是 pytest 的失敗訊息會把它壞在哪講得比較清楚。

第三個誤會是「測試框架的選擇很重要」。對專案的成敗來說,它排在很後面。重要的是有沒有測試、測試跑不跑得快、有沒有人在維護它們。這篇講的「第一個相依」是專案結構的問題,不是測試的問題,測試只是那個問題最常出現的地方。

一個週末可以試的小練習

寫一個二十行的測試執行器,不用任何套件。unittest.TestLoader().discover("tests") 找出所有測試,unittest.TextTestRunner(verbosity=2).run(suite) 跑它們。然後加一個參數:傳 fast 只載入 tests/test_core*.py,傳 full 全部載入。再加一個:如果環境變數 CI 存在,跳過任何名字裡有 local 的測試。

你會發現 pytest 的 -k、marker、外掛做的很多事,用標準函式庫二十行就有一個夠用的版本。這不是說 pytest 沒有價值,是說「我需要 pytest」和「我需要一個能挑測試跑的東西」是兩句不同的話。分清楚這兩句話,你就知道自己的專案站在哪一階。

後記

大學畢業前那張書桌,最後一個學期我試過清空一次。清完的那個晚上很舒服,第二天早上放了一杯水,第三天放了一本書,第四天就回不去了。我後來接受了一件事:零是一個很好的起點,但不是一個能待很久的地方。專案也一樣,零相依很美,但大部分認真的專案最後都會有相依,重點是第一個是什麼時候、為什麼進來的。知道那個時刻,比守住零更重要。

官方網站:docs.pytest.org、docs.python.org unittest

閱讀偏好

返回全部文章作者與內容更正

留言(0)

還沒有留言。讀完有想法,歡迎留下第一則。

留言經站長審核後才會顯示。暱稱和留言會公開,請不要寫下電話、地址等個人資料;資料怎麼處理,請見隱私權政策。