iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Software Development

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

Day 30:總結——這 63 支測試教我的分層策略

  • 分享至 

  • xImage
  •  

前言:為什麼想寫「一套系統的測試策略」而不是「測試語法教學」

市面上不缺「Laravel 怎麼寫測試」的文章——assertOk() 怎麼用、Factory 怎麼建、Mock 怎麼寫,這些內容查文件就找得到。但很少有文章願意攤開一套真實系統的測試檔案,誠實地說「這裡測得很好」跟「這裡測試覆蓋率數字漂亮,但保護力有落差」。

寫這個系列的動機很單純:我剛好接手維護一個真實的系統,它同時要維護兩個對外介面版本、後台用 Filament 開發、還要跟一個我完全無法控制的外部系統(校務系統的 SOAP API)交換資料。這樣的組合逼著我認真想清楚「測試該分幾層」這個問題,而不是照抄教科書上「Unit 測純邏輯、Feature 測整合」這句話就結束。30 天走下來,我想把這個思考過程完整攤開,包含走對的地方,也包含走岔的地方。

第一部回顧:測試分層與基礎設施(Day 1-7)

這一部從系統的整體樣貌談起:63 支測試檔案裡,Unit 只有 7 支,其餘全部是 Feature——這個懸殊的比例本身就在提示一件事,這個系統裡「純邏輯、不碰資料庫」的程式碼天生就不多。接著看了測試框架從 PHPUnit 遷移到 Pest 留下的痕跡、asAdmin()/asSuperAdmin() 這類測試輔助函式怎麼幫全專案省下重複程式碼、Model Factory 的語意化設計、以及「migration 內建資料」跟「測試裡用 Factory 建資料」之間的取捨。這一部的核心觀察是:測試分層不是拿著規則硬套,而是先誠實面對「這個系統真正的純邏輯有多少」。

第二部回顧:Feature 測試與後台測試(Day 8-15)

第二部深入 Feature 測試怎麼寫,包含 Filament 後台的測試手法(重點是「測動作有沒有真的發生」而不是「測畫面長什麼樣」)、巢狀資源的獨立模式跟內嵌模式各自怎麼測、以及一個特別值得記住的觀察——**這個系統完全沒有針對 Policy class 的 unit test,全部靠 Filament Resource 的行為測試間接驗證授權邏輯有沒有生效。**這一部埋下了整個系列最重要的伏筆:Day 14 那支被 skip() 掉的登入測試,測試檔案存在、也顯示綠燈,但真正涉及 SSO 登入流程的案例整支被跳過,只驗證了「這個路由不是 404」——測試覆蓋率的數字,跟它實際帶來的保護力,中間有一段沒被填上的落差。

第三部回顧:測試替身與外部整合測試(Day 16-23)

第三部處理這個系統最特別的地方:跟一個你完全無法控制的外部系統交換資料,該怎麼測。核心是兩支自製的測試替身——FakeHttpClient 跟 FakeCaller,分別處理標準 HTTP 層跟 SOAP 層,共用同一份錄製下來的 cassette 檔案,依 SOAPAction header 存不存在分流消費。這一部也回頭呼應了 Day 14 的伏筆:Day 19 看到 SOAP Client 底層元件的單元測試寫得相當紮實,但底層元件測得仔細,不代表整合起來的登入行為就有安全網保護——這正是 Day 14 那支被跳過的測試留下的破口。這一部也誠實記錄了一個素材偏薄的案例:Crawler 目前只有 happy path 測試,還沒涵蓋外部網頁格式跑掉這種邊界情況,這不是要硬掰出不存在的測試,而是誠實標注「這裡的覆蓋還不夠」。

第四部回顧:Job/Command 測試與總結(Day 24-30)

最後一部從時間相依的 Job 測試開始——Carbon::setTestNow() 這個容易被忽略、卻能讓測試結果不再看日曆臉色的技巧。接著是一個真實發生過的重構故事:Day 22 那支「拿掉假 unit test」的案例,舊版測試手動塞資料庫紀錄,完全繞過了套件本身的邏輯,新版測試改成真的走一次上傳流程,才是一支真正能保護系統的測試。這一部也發現了整個系列最值得記住的測試覆蓋盲點——/sitemap.xml 這個真正輸出給搜尋引擎讀的路由,反而完全沒有 Feature 測試直接驗證,這個系統真的因此出過一次 Content-Type bug,是靠人工事後排查才發現的。

一個誠實的失敗經驗

如果這個系列只講「這個系統的測試策略設計得很好」,那就違背了我一開始想做的事。老實說,這 30 天拆下來,最讓我不舒服的不是找到多少值得學習的好模式,而是發現:一個看起來測試檔案數量不少、覆蓋率數字應該不難看的系統,真正重要的保護漏洞,反而藏在最容易被忽略的地方——一支被 skip() 掉、沒有人去問「為什麼要跳過」的登入測試;一個沒有人會手動點開檢查、只有搜尋引擎機器人會碰的端點。

這兩個案例讓我意識到一件更根本的事:**測試覆蓋率的分布,反映的是「開發者平常注意力放在哪裡」,不是「哪裡真正重要」。**開發者天天盯著看的頁面(首頁、登入頁的畫面),測試自然比較齊全;使用者幾乎不會主動碰的端點(/sitemap.xml)、被標成 skip() 之後就沒人再回頭處理的測試案例,反而是保護力真正的破口所在。這件事沒有簡單的解法——我沒有在這 30 天裡把那支被跳過的登入測試補完,也沒有幫 /sitemap.xml 補上測試,這兩件事誠實地留在「還沒解決」的狀態。寫這個系列的過程,某種意義上也是幫自己列出一份接下來真正該優先處理的待辦清單,而不是假裝我已經把系統的每個角落都照顧到了。

回到系列主題句

這個系列一開始立下的主題句是:同一套系統要維護兩個介面版本、還要跟一個不受自己控制的外部系統交換資料,測試分層的意義不是「規則要求要分 Unit/Feature」,而是「哪一層的失敗該讓你立刻知道是自己程式壞了,哪一層的失敗只是外部系統的資料長得跟預期不一樣」。

30 天寫完,我想把這句話再具體拆成三層分工:Unit 測試(純邏輯、格式轉換這類不碰資料庫、不碰外部依賴的程式碼)回答「這段運算邏輯本身對不對」;Feature 測試(Job、Command、HTTP 端點,牽涉資料庫跟真實流程)回答「這個系統自己的行為對不對」;VCR/Fake 測試替身(錄製外部系統的真實回應當 fixture)回答「當外部系統回應長得不如預期時,我這邊的程式碼有沒有正確反應」。這三層各自對應不同的問題,一旦某一層的測試變紅,你應該立刻知道要往哪個方向排查——而不是每次紅燈都要從頭把整個系統重新想一遍。

給讀者的實用建議

如果你也想把這套思考方式套進自己的專案,幾個立即可行的第一步:

  • 先問一句「我的專案裡,哪些外部依賴是我能控制的,哪些不是」——能控制的(自己的資料庫、自己的業務邏輯)用 Factory/資料庫直接測;不能控制的(第三方 API、外部系統)用錄製重播或 Fake 隔絕依賴,不要為了測試方便去改動你控制不了的介面。
  • 找一支被你標成 skip() 卻很久沒人回頭處理的測試,問自己「為什麼當初要跳過它」——如果理由已經不成立,這可能就是你系統裡最該優先補上的保護缺口。
  • 列出你系統裡「幾乎沒有人類使用者會手動碰」的端點或功能(排程任務、給機器讀的輸出、webhook),這些地方測試覆蓋率天生就容易被忽略,值得反過來優先檢查。

漸進式的技能路徑:先從分清楚「純邏輯」跟「牽涉外部依賴」這兩種程式碼練起;等這個判斷變得直覺了,再往「怎麼設計測試替身」這個層次推進;最後才是回頭盤點測試覆蓋率的分布,找出那些「數字好看但保護力不足」的地方——這個層次的判斷,通常要親自被一次「明明有測試卻沒攔住的 bug」教訓過,才會真正記住。

結語

回到 Day 01 那個開場問題:測試不是分 Unit 跟 Feature 兩層就好了嗎?寫完這 30 天,我的答案沒有變得更複雜,但變得更誠實了——**分幾層從來不是重點,重點是每一層測試紅燈的時候,你能不能立刻知道「這代表什麼」。**一個系統的測試策略設計得再精巧,如果沒有人願意誠實面對「這裡測試覆蓋率數字好看,但其實沒有真的保護到什麼」,那些精巧的設計也只是自我安慰。

這套分層思考還在持續修正——那支被跳過的登入測試、那個沒有測試的 /sitemap.xml,都還在我的待辦清單上。如果你在自己的專案裡也找到過類似「測試存在但保護力不足」的地方,很樂意聽聽你是怎麼發現、又是怎麼處理的。謝謝你陪我走完這 30 天。


上一篇
Day 29:首頁測試——一個頁面測試要先準備多少「全站狀態」
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言