「這段排序邏輯已經跑了很久,應該沒問題吧?」
這句話幾乎是每個維護老專案的人都說過的話。今天要講的案例,剛好打臉了這句話:這個系統的首頁排序邏輯,實際上早就被改壞了,而且壞了一段時間都沒人發現——直到有人補上一支原本沒有的 Feature 測試,才把這個問題揪出來。
這個系統的首頁有一個「headline 新聞」區塊,排序規則本來的設計意圖是:新聞類型的內容優先顯示、置頂內容優先於一般內容、同一層級再依發佈日期排序。這套排序邏輯本身已經存在一段時間,維護者也一直相信它照設計運作。
轉折點發生在有人決定幫這個排序行為補上一支 Feature 測試——不是因為懷疑它壞了,單純是覺得「這麼重要的排序邏輯,怎麼可以沒有測試」。測試寫好、資料準備好、跑下去,結果紅燈:排序結果跟預期的優先序不一致。
追下去才發現,排序邏輯確實在某次改動裡被動過,而那次改動意外破壞了「類型優先」這條規則。因為沒有測試守著,這個回歸沒有在改動當下被發現,一直安靜地留在正式環境裡,直到補測試的人意外揭發它。
修法很直接:把類型優先的排序條件加回去,恢復原本的設計意圖。但這個案例真正有價值的地方,不是「怎麼修」,而是「怎麼發現」——測試不是寫給還沒發生的未來問題準備的,它同樣有能力揭發已經發生、卻沒人注意到的問題。
排序邏輯聽起來像是一段「純邏輯」,直覺上似乎該寫成 Unit 測試。但這裡的排序,實際上是透過 Eloquent 查詢的排序條件(orderBy、自訂的排序 Scope)組合出來的,最終要驗證的是「從資料庫查出來的這批資料,順序對不對」。
這意味著要驗證這段邏輯,得先真的準備好一批帶有不同類型、不同置頂狀態、不同發佈日期的測試資料寫進資料庫,再檢查查詢結果的順序——這正是 Day 6 提到的判斷標準:依賴資料庫查詢結果的邏輯,適合寫成需要 RefreshDatabase 的測試,而不是純記憶體物件測試。
更進一步,這支測試選擇直接測「首頁這個 HTTP 端點回傳的畫面,資料出現的先後順序對不對」,而不是只測「排序 Scope 這個小單位本身」。這個選擇也有它的道理:排序邏輯最終要對使用者呈現的是首頁畫面上的視覺順序,如果只測 Scope 本身回傳的 Collection 順序,沒辦法保證這個順序真的會反映在畫面上——中間可能還經過 View 渲染邏輯、資料轉換,任何一個環節都可能讓「Scope 排序正確」跟「畫面顯示順序正確」出現落差。直接測到 HTTP 回應層級,驗證的是使用者最終真正會看到的結果。
這個系統的 Feature 測試裡,有幾個常用的自訂斷言方法,統一定義在測試設定檔裡,讓測試程式碼可以直接呼叫,例如驗證「這次請求最終對應到哪一個具名路由」、「最終是哪一個 Controller 處理的」。
沒有這些自訂斷言時,你可能得這樣寫:
$response = get('/');
expect($response->status())->toBe(200);
// 想確認對應到哪個路由,得自己想辦法從 request 物件挖出來
有了語意化的自訂 expectation,測試讀起來更直接:
get('/')
->assertOk()
->toMatchRoute('home')
->toMatchController('HomeController');
這種把常用斷言邏輯包裝成語意化方法的做法,價值不只是少打幾行程式碼,而是讓測試案例本身讀起來更接近「這支測試在驗證什麼」,而不是「這支測試在操作什麼技術細節」。當同樣的斷言邏輯會在幾十支測試裡重複出現時,集中定義一次、每個地方都用同一種語意化的方式呼叫,也讓日後要調整斷言邏輯時,只需要改一個地方。
維護者心態:這段排序邏輯已經在正式環境跑了很長一段時間,
使用者也沒有明顯反應排序不對,應該是沒問題的,
不需要特別花時間補測試。
→ 「沒有人抱怨」不等於「邏輯正確」,
可能只是排序錯誤的影響不夠明顯,
或使用者根本不知道正確順序該是什麼樣子,
沒有測試意味著這類問題永遠不會被主動發現
維護者心態:不確定排序邏輯現在是不是真的照設計運作,
補一支測試,明確寫下「類型優先、置頂優先、
發佈日期次之」這個排序規則該有的樣子,
讓測試結果告訴我答案,而不是靠印象判斷。
→ 補測試的過程本身,就是一次對「這段邏輯還符不符合
設計意圖」的主動查證,結果發現規則早就被破壞,
而且是在測試補上的當下才第一次被發現
這兩種心態的差別,決定了一個系統裡「早就存在但沒人知道」的問題,會不會有機會被揪出來。 「這段程式碼跑了很久沒出事」從來不是「這段程式碼是對的」的證據,只是「還沒有人主動去查證」的證據。
回想你手上維護的專案,有沒有一段核心邏輯是「大家都相信它是對的,但其實沒有測試守著」?如果現在花時間幫它補一支測試,你有多大把握測試會一次就綠燈通過?
RefreshDatabase 的 Feature 測試,直接驗證 HTTP 回應層級的順序,而不只是內部 Scope 的結果明天要從最基礎的 Feature 測試講起——一個 HTTP 請求,怎麼從「回應狀態碼對不對」進階到「畫面上真的顯示了我要的資料」,兩種驗證力度的差別在哪裡。