持續整合需要頻繁執行建置與測試。每次只改一小部分程式,卻要等十幾分鐘才能取得結果,就可能讓開發者放慢提交頻率。若主幹上的某次提交造成建置失敗,在結果回報之前,其他人也可能已經取得這個版本繼續開發。
我在 PHP 專案中也遇過 CI 流程的主要時間花在建置與測試的情況。檢查耗時時,除了受測程式本身,還要看測試啟動了哪些功能、建立了哪些連線,以及如何準備測試環境。
測試耗時不全都花在受測程式上。除錯、收集覆蓋率資料,以及啟動時建立的連線,也會增加執行時間。可以先檢查這次測試需要哪些功能與服務,再停用不需要的功能,或調整建立連線的時機。
我先前是在不需要除錯與覆蓋率資料的測試指令中,將 Xdebug 設為 off,停用它提供的功能。
如果受測程式沒有使用 Redis,應用程式卻在每次啟動時先連上它,測試就會反覆花時間建立不需要的連線。我曾調整連接 Redis 的機制,減少這段耗時。
測試由單一程序依序執行時,要等前面的測試結束,才會開始下一個。若執行環境有多個核心,可以把測試分配給多個程序,同時執行,縮短整批測試的等待時間。不過,要先分開各程序會修改的資料,避免測試互相影響。
我原本使用 PHPUnit 依序執行測試,後來改用 ParaTest 平行執行。這個專案的測試會修改資料庫,因此準備環境時,也為各個程序建立分開的測試資料庫。
當時本機使用 M1 Pro、10 核心,紀錄如下。make up 是專案準備測試環境的指令,make test 則執行測試:
| 階段 | 改為平行測試前 | 改為平行測試後 |
|---|---|---|
準備環境:make up |
9.0 秒 | 27.7 秒 |
執行測試:make test |
4 分 32 秒 | 27.5 秒 |
把兩個階段的時間加起來,這次調整仍有改善,但只看 make test 就會漏掉新增的準備時間。
這是當時本機環境的量測紀錄,不是完整 CI 流程的耗時。CI 環境使用的核心數不同,也可能需要另外下載依賴或準備成品,不能直接沿用本機的加速幅度。
建立測試資料庫時,如果每次都先載入最初的結構,再依序執行後續變更,就會反覆花時間重做相同的步驟。當測試需要的是目前的資料庫結構時,可以考慮直接載入完整結構,省下重複執行歷史變更的時間。
平行測試增加環境準備時間後,我就嘗試用這個方式改善建立資料庫的步驟,量測結果確實變快。
但正式環境仍需要逐步套用結構變更,測試環境則要另外更新完整結構。團隊需要先約定如何維護這兩份內容,因此我最後先撤回這個方案,保留量測紀錄供後續討論。
測試反覆讀寫外部資料庫時,除了查詢本身,也要花時間傳送請求與接收結果。改用測試程序內的記憶體資料庫,可以減少這些往返,但還要確認替換後的查詢行為是否符合原本的測試需要。
我曾把主要測試資料庫從 MySQL 換成記憶體內的 SQLite,先轉換資料庫結構,再調整連線與測試設定。
調整後雖然縮短了執行時間,卻因字串比對差異,以及測試依賴未保證的排序順序,讓部分測試失敗。繼續採用還要維護另一套資料庫結構與設定,考量這些問題與維護成本,我最後撤回這個嘗試,繼續使用 MySQL。
測試輸入規則時,如果每種輸入組合都透過完整請求驗證,就會反覆執行應用程式啟動、路由與中介層等步驟。可以把輸入規則分開測試,直接確認資料是否符合規則,再以整合測試確認請求流程,減少反覆啟動應用程式的成本。
我曾在 Laravel 12 專案中調整 FormRequest 測試。FormRequest 負責請求的輸入驗證,原本的測試會送出 HTTP 請求,再檢查回應。後來,我改用單元測試,直接使用 Laravel 的 Validator 驗證各種輸入組合,並調整取得依賴的方式,讓這些測試不必啟動應用程式。
請求是否使用這些規則,以及權限判斷與錯誤回應,仍應由整合測試確認。
改善測試效能,需要找出時間花在哪裡,也要評估改動後增加的準備與維護成本。
多人同時準備合併時,即使測試變快,仍可能在自己的測試完成前,已有其他提交合併到主幹。接下來需要確認的,是實際準備合併的版本組合是否通過驗證。