「這個專案的測試是用 Pest 寫的還是 PHPUnit 寫的?」
如果你隨手打開這個系統的 tests/ 目錄亂逛,答案看起來很明確:滿眼都是 test()、it()、describe() 這種 Pest 語法,偶爾夾雜一支 class ExampleTest extends TestCase 的舊式寫法,像是誰忘了收拾的殘局。昨天我差點把這篇寫成「這個專案怎麼讓 Pest 跟 PHPUnit 兩套語法並存」,翻了一次 commit 歷史才發現自己想錯方向——這裡沒有「並存」這回事,只有一次還沒收尾乾淨的遷移。
今天要講的不是「Pest 跟 PHPUnit 怎麼選」,而是一件更貼近真實維護現場的事:框架升級這種大範圍的改動,即使目標明確、也真的做完了,還是會留下一些沒被順手清掉的痕跡,而這些痕跡本身就是判斷這個專案測試成熟度的線索。
tests/Pest.php 統一設定測試基底去查這個專案的 commit 歷史,會找到一個訊息寫著「升級到 Laravel 12,Pest v3,並把外部服務錄製重播機制換成自製測試替身」的 commit。光看這一行訊息,就能拆出三件表面上不相關、但其實同一次動手做掉的事:
這件事想提醒的是:大範圍升級很少是單一動作,通常是「既然都要動這麼多測試檔案了,順便把另一件一直想做但沒空做的事也做掉」的組合拳。這也解釋了為什麼 commit 歷史裡還能找到更早一支專門「統一測試風格,全面採用 Pest 語法輔助函式」的 commit——遷移本身也不是一次到位,是分階段推進的。
回到那支還在用 class ExampleTest extends TestCase 寫法的檔案。它是 Laravel 專案初始化時框架自動產生的範例測試檔,內容通常只是驗證「應用程式能不能正常回應一個請求」這種最基本的健康檢查,本身沒有業務邏輯。
它為什麼沒被一起遷移成 Pest 語法?合理的推測是:這支測試從頭到尾沒人真的去動它,遷移的心力自然會優先花在有業務邏輯、會隨著功能開發持續變動的測試檔案上,一支從沒改過的範例測試,順位自然排到最後——甚至排到「還沒排到」。
這不是缺陷,而是真實專案裡「遷移優先序」很自然的樣子:先遷移會被頻繁修改、直接關係到功能正確性的測試,範例性質、幾乎不會再被碰的檔案,晚一點處理甚至不處理,都是合理的判斷,只要不影響測試套件整體能不能執行。
Pest.php 做了什麼Pest 有一個 PHPUnit 沒有的機制:tests/Pest.php 這支設定檔可以用 uses() 幫一整個資料夾底下的測試統一套用共用的 TestCase 跟 Trait,不用每支測試檔案自己 use 一次。
這個專案的 Pest.php 對 Feature 資料夾底下所有測試統一套用了專案自訂的 TestCase,並疊加了資料庫刷新機制,確保每個測試案例執行前資料庫都是乾淨狀態。這一行設定的意義是:把「每支測試都要記得刷新資料庫」這種容易被遺忘的樣板設定,從「開發者責任」降級成「框架設定,自動生效」。少了這一步,理論上還是可以在每支測試檔案裡各自手動宣告,但那樣的維護成本會隨著測試檔案數量增加而線性上升——這個系統目前有 63 支測試檔案,如果每一支都要自己記得設定一次,出錯的機率不會是零。
看到專案裡同時有 Pest 跟 PHPUnit 語法的測試檔案,
直接下結論:「這個團隊選擇讓兩套語法並存,
可能是想漸進式引入 Pest,不強迫全部改寫」。
→ 沒有查 commit 歷史,憑檔案現狀直接推測意圖,
容易把「還沒做完的事」誤判成「刻意的設計決策」
看到同樣的現狀,先跑一次 git log 對照這個檔案的歷史,
發現整個測試套件在某個時間點集中遷移到 Pest,
只有一支從未被修改過的範例測試沒被一起處理。
→ 正確結論:這是一次遷移的殘留,
不是「刻意維持兩套語法」的設計選擇,
這支殘留檔案的優先序低到目前還沒被排上議程
同一個現狀,兩種完全不同的判斷結果——差別只在有沒有去查一次歷史。 這個習慣不只適用於框架遷移,任何時候看到程式碼裡「看起來不一致」的地方,先假設它有一段沒被記錄下來的過程,比直接假設「這是刻意的」更接近真相。
回想你手上維護的專案,有沒有一次「大範圍升級」其實暗藏了其他順便做掉的改動?如果現在有新人看到那次升級的 diff,他們有辦法從 commit 訊息裡看出這件事,還是要靠口耳相傳才知道?
tests/Pest.php 用 uses() 統一設定測試基底,把容易被遺忘的樣板設定從開發者責任降級成框架自動生效明天要講這個系統怎麼設計測試輔助函式——兩個看起來只是「省重複程式碼」的函式,背後其實藏著一個關於「測試環境要不要跟著遵守最小權限原則」的判斷。