fixture 與 conftest:管好 driver 的生命週期
#pytest #fixture #conftest #QA 實戰
每個測試檔都寫一次 driver = webdriver.Remote(...)、測完再 driver.quit()。有二十個測試模組,就有二十個地方要同步改。driver 的建立和關閉,應該交給 pytest fixture。 conftest.py 放共用 fixture pytest 在執行前會自動掃描 conftest.py,裡面定義的 fixture 對同層與子目錄的所有測試模組都可用, 不需要 import 。 測試檔直接宣告參數名,pytest 自動注入: function vs session scope fixture 預設是 scope="function",每個測試函式各自拿到一個全新 driver。 優點 :隔離乾淨,測試 A 留下的狀態不影響測試 B。 缺點 :啟動 App 約 3–8 秒,一百個測試就是 5–13 分鐘的純啟動成本。 換成 scope="session",整個 pytest 執行過程只開一個 driver: 優點 :省下大部分的啟動時間。 缺點 :測試之間共享狀態:一個測試改了登入帳號、留在某個頁面,下一個測試可能就出錯。 選哪個? 寫入型、改變 App 狀態的測試用 function;只讀、純查看介面的測試才考慮 session。混合使用時,把兩種 scope 各定義一個 fixture,讓測試自己選參數名。 保證清理:yield 後一定 quit yield 把 fixture 切成「前置」與「後置」兩段:後置段 即使測試失敗、拋例外也一定執行 ,等同 try/finally: 不用 try/finally 的話,yield 本身已保證後置段執行;但如果你在 fixture 裡還有其他可能拋出的初始化邏輯,用 try/finally 更保險。 疊 fixture:登入狀態重用 把登入動作包成獨立 fixture,讓需要登入才能操作的測試不必自己呼叫登入步驟: 注意:疊在上面的 fixture scope 不能比它依賴的 fixture 更廣。若 driver 是 function scope,logged in driver 也必須是 function scope 或更窄;反過來宣告 scope="session" 會讓 pytest 拋出 ScopeMismatch 錯誤。 測試宣告 logged in driver 就能直接操作登入後的頁面: 每個測試各自拿到一個全新的已登入 driver,登入步驟由 fixture 負責,測試主體保持乾淨。 我選了 session scope,代價是 teardown 上面建議「寫入型測試用 function scope」。我實際的專案裡,driver 是 session scope ,整輪測試共用一個 session,包括會改變狀態的測試。 原因很現實:App 啟動一次太貴,測試又多。每支都重開,整輪跑下來的時間我承受不起。 代價是測試之間會互相污染:上一支停在商店頁,下一支以為自己在首頁,一開始就錯。我的解法是讓每支測試 宣告自己結束後要怎麼收尾 ,不重開 App: 收尾的邏輯統一寫在 fixture 的 yield 之後,照 marker 決定要做什麼: yield 之後的程式碼在測試失敗時也會執行,這是整套機制能成立的關鍵。測試本體完全不寫清理,只宣告自己屬於哪一種收尾。 這套 marker 現在種類不少:一般返回、登出、購買後返回、離開多人連線畫面……一開始我覺得種類太多,後來發現這反而是好事:每一種都對應一個真實的「這支測試會把 App 弄成什麼狀態」,看 marker 就知道這支測試的副作用是什麼。 一個讓我等了好幾分鐘的 assert True fixture 多了之後,我把它們拆成好幾個 plugin 檔:driver、報表、log、錯誤攔截、錄影、crash log、logcat 各一個。其中好幾個是 autouse=True,而且都依賴 driver。 問題是 fixture 的依賴是無條件的。只要有一個 autouse fixture 依賴 driver, 任何 pytest 呼叫都會啟動整套裝置機制。我後來想幫框架的工具函式寫單元測試,結果一支 assert True 也要連真機、起 iOS 的 companion 服務、清 WDA 的程序,好幾分鐘跑不完。 修法是加一道閘門:這些 autouse fixture 先判斷這支測試需不需要裝置,不需要就什麼都不做;需要才去拿 driver。 預設行為不變,UI 測試照舊;只有放在 test/unit/ 或標了 @pytest.mark.no device 的測試會跳過。單元測試從好幾分鐘降到不到一秒。 省了幾分鐘還是其次。測試跑得慢,人就會少跑;少跑,問題就晚發現。開發時的回饋迴圈越快,越願意常跑。 一個會靜默失效的坑 最後一個小坑。conftest 裡的 hook 函式,例如 pytest runtest logreport, 同一個模組只能有一個 。如果你寫了第二個同名函式,Python 會直接用後面的覆蓋前面的,不會報錯,前一個就這樣不見了。 我在加 flaky 追蹤功能的時候差點踩到:想另外寫一個 hook 來收集重跑資料,才發現 conftest 裡已經有一個了。正確做法是擴充既有的那個,不要另開一個。