iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

同一套 Laravel 系統,測試怎麼寫才不會說謊系列 第 7

Day 07:一次排序邏輯的回歸——Feature 測試補上後才抓到的舊 bug

  • 分享至 

  • xImage
  •  

前言

「這段排序邏輯已經跑了很久,應該沒問題吧?」

這句話幾乎是每個維護老專案的人都說過的話。今天要講的案例,剛好打臉了這句話:這個系統的首頁排序邏輯,實際上早就被改壞了,而且壞了一段時間都沒人發現——直到有人補上一支原本沒有的 Feature 測試,才把這個問題揪出來。

今日目標

  • 看一次真實的「補測試才發現舊 bug」案例,理解測試的價值不只是防止未來壞掉,也能抓出已經壞掉的東西
  • 理解為什麼這個排序邏輯的驗證,適合寫成 Feature 測試而不是 Unit 測試
  • 認識這個系統怎麼用自訂 expectation 讓 Feature 測試斷言更語意化
  • 學到一個心態:「這段程式碼跑了很久沒出事」不等於「這段程式碼是對的」

案例:補測試,發現排序早就壞了

這個系統的首頁有一個「headline 新聞」區塊,排序規則本來的設計意圖是:新聞類型的內容優先顯示、置頂內容優先於一般內容、同一層級再依發佈日期排序。這套排序邏輯本身已經存在一段時間,維護者也一直相信它照設計運作。

轉折點發生在有人決定幫這個排序行為補上一支 Feature 測試——不是因為懷疑它壞了,單純是覺得「這麼重要的排序邏輯,怎麼可以沒有測試」。測試寫好、資料準備好、跑下去,結果紅燈:排序結果跟預期的優先序不一致

追下去才發現,排序邏輯確實在某次改動裡被動過,而那次改動意外破壞了「類型優先」這條規則。因為沒有測試守著,這個回歸沒有在改動當下被發現,一直安靜地留在正式環境裡,直到補測試的人意外揭發它。

修法很直接:把類型優先的排序條件加回去,恢復原本的設計意圖。但這個案例真正有價值的地方,不是「怎麼修」,而是「怎麼發現」——測試不是寫給還沒發生的未來問題準備的,它同樣有能力揭發已經發生、卻沒人注意到的問題

為什麼這是 Feature 測試,不是 Unit 測試

排序邏輯聽起來像是一段「純邏輯」,直覺上似乎該寫成 Unit 測試。但這裡的排序,實際上是透過 Eloquent 查詢的排序條件(orderBy、自訂的排序 Scope)組合出來的,最終要驗證的是「從資料庫查出來的這批資料,順序對不對」。

這意味著要驗證這段邏輯,得先真的準備好一批帶有不同類型、不同置頂狀態、不同發佈日期的測試資料寫進資料庫,再檢查查詢結果的順序——這正是 Day 6 提到的判斷標準:依賴資料庫查詢結果的邏輯,適合寫成需要 RefreshDatabase 的測試,而不是純記憶體物件測試

更進一步,這支測試選擇直接測「首頁這個 HTTP 端點回傳的畫面,資料出現的先後順序對不對」,而不是只測「排序 Scope 這個小單位本身」。這個選擇也有它的道理:排序邏輯最終要對使用者呈現的是首頁畫面上的視覺順序,如果只測 Scope 本身回傳的 Collection 順序,沒辦法保證這個順序真的會反映在畫面上——中間可能還經過 View 渲染邏輯、資料轉換,任何一個環節都可能讓「Scope 排序正確」跟「畫面顯示順序正確」出現落差。直接測到 HTTP 回應層級,驗證的是使用者最終真正會看到的結果。

自訂 expectation:把重複斷言包裝成語意化 API

這個系統的 Feature 測試裡,有幾個常用的自訂斷言方法,統一定義在測試設定檔裡,讓測試程式碼可以直接呼叫,例如驗證「這次請求最終對應到哪一個具名路由」、「最終是哪一個 Controller 處理的」。

沒有這些自訂斷言時,你可能得這樣寫:

$response = get('/');

expect($response->status())->toBe(200);
// 想確認對應到哪個路由,得自己想辦法從 request 物件挖出來

有了語意化的自訂 expectation,測試讀起來更直接:

get('/')
    ->assertOk()
    ->toMatchRoute('home')
    ->toMatchController('HomeController');

這種把常用斷言邏輯包裝成語意化方法的做法,價值不只是少打幾行程式碼,而是讓測試案例本身讀起來更接近「這支測試在驗證什麼」,而不是「這支測試在操作什麼技術細節」。當同樣的斷言邏輯會在幾十支測試裡重複出現時,集中定義一次、每個地方都用同一種語意化的方式呼叫,也讓日後要調整斷言邏輯時,只需要改一個地方。

❌ 相信「跑了很久沒出事」等於「邏輯是對的」

維護者心態:這段排序邏輯已經在正式環境跑了很長一段時間,
使用者也沒有明顯反應排序不對,應該是沒問題的,
不需要特別花時間補測試。

→ 「沒有人抱怨」不等於「邏輯正確」,
  可能只是排序錯誤的影響不夠明顯,
  或使用者根本不知道正確順序該是什麼樣子,
  沒有測試意味著這類問題永遠不會被主動發現

✅ 把「應該正確」的假設,變成可驗證的測試

維護者心態:不確定排序邏輯現在是不是真的照設計運作,
補一支測試,明確寫下「類型優先、置頂優先、
發佈日期次之」這個排序規則該有的樣子,
讓測試結果告訴我答案,而不是靠印象判斷。

→ 補測試的過程本身,就是一次對「這段邏輯還符不符合
  設計意圖」的主動查證,結果發現規則早就被破壞,
  而且是在測試補上的當下才第一次被發現

這兩種心態的差別,決定了一個系統裡「早就存在但沒人知道」的問題,會不會有機會被揪出來。 「這段程式碼跑了很久沒出事」從來不是「這段程式碼是對的」的證據,只是「還沒有人主動去查證」的證據。

今日思考題

回想你手上維護的專案,有沒有一段核心邏輯是「大家都相信它是對的,但其實沒有測試守著」?如果現在花時間幫它補一支測試,你有多大把握測試會一次就綠燈通過?

今日重點回顧

  • 一次補測試的過程,意外發現首頁排序邏輯早就被某次改動破壞,測試的價值不只是防未來,也能揪出已經存在的問題
  • 排序邏輯依賴資料庫查詢結果,適合寫成需要 RefreshDatabase 的 Feature 測試,直接驗證 HTTP 回應層級的順序,而不只是內部 Scope 的結果
  • 自訂 expectation 把重複的斷言邏輯包裝成語意化方法,讓測試案例讀起來更貼近「在驗證什麼」而不是「在操作什麼」
  • 核心心態:「這段程式碼跑了很久沒出事」不等於「這段程式碼是對的」,只是還沒被主動查證

明日預告

明天要從最基礎的 Feature 測試講起——一個 HTTP 請求,怎麼從「回應狀態碼對不對」進階到「畫面上真的顯示了我要的資料」,兩種驗證力度的差別在哪裡。


上一篇
Day 06:Model 的兩種純邏輯單元測試——accessor 跟 Query Scope
下一篇
Day 08:Feature 測試基礎——一個 HTTP 請求怎麼測到「畫面對不對」
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言