iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
Software Development

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

Day 01:系列介紹——同一套 Laravel 系統,測試怎麼寫才不會說謊

  • 分享至 

  • xImage
  •  

前言

「測試不是分 Unit 跟 Feature 兩層就好了嗎?為什麼還要另外花 30 天講這件事?」

如果你的專案只有一套對外介面、不用跟任何外部系統交換資料,這個問題的答案可能真的就是「分兩層就夠了」。但如果你手上的系統同時要維護兩個版本的對外畫面,還要跟一個你完全無法控制、也無法左右它什麼時候壞掉的外部系統持續交換資料,「測試該分幾層」這個問題就不再只是規則問題,而是一個關於「哪一層的紅燈該讓你立刻緊張,哪一層的紅燈只是在告訴你外部世界又長得不一樣了」的判斷問題。

這個系列要用一套真實運作中的 Laravel 系統當教材——一個大學的入口網站,前台同時維護新舊兩套介面版本,後台用 Filament 開發,還要透過 SOAP 協議跟學校既有的校務系統交換使用者資料跟單一登入資訊。這套系統目前累積了 63 支測試檔案,橫跨 Unit、Feature、還有一組用來錄製/重播外部系統回應的測試替身機制。接下來 30 天,我會逐一拆開這些測試檔案,看它們實際在測什麼、為什麼這樣分層、以及哪些地方測試覆蓋率數字很漂亮但保護力其實有落差。

先講清楚一件事:這個系列不是「Laravel 測試語法教學」,市面上這類文章已經很多。這個系列要做的是把一套真實系統的測試策略攤開來看——包含它做對的地方,也包含它測試覆蓋率數字好看、但實際保護力有落差的地方,兩種案例都會誠實呈現。

今日目標

  • 認識這次實驗的素材系統:一個維護兩版前台、一個 Filament 後台、還要對接外部 SOAP 系統的真實 Laravel 專案
  • 理解「測試要分幾層」不是規則問題,而是「哪一層的失敗該讓你立刻反應」的判斷問題
  • 初步認識 63 支測試檔案的真實分佈:Unit 只有 7 支,其餘全部是 Feature
  • 認識這個系列會反覆使用的核心判斷句,作為後面 29 篇的共同基準
  • 預告接下來 30 天的四大部架構

這套系統為什麼值得拿來講測試分層

先講系統本身的形狀。這是一個大學的入口網站,前台同時掛著兩套介面版本(先叫它 V1、V2)——舊版介面還在服務一部分使用者,新版介面已經上線但還沒完全取代舊版,兩套版本背後共用同一批 Model(新聞、頁面、輪播 Banner、快速連結),但畫面跟部分互動邏輯是分開維護的。後台是用 Filament 開發的管理介面,維護者在這裡管理內容、審核使用者權限。

再往下一層,這個系統還要透過 SOAP 協議跟學校既有的校務系統交換資料:使用者的單一登入(SSO)驗證、帳號資訊查詢,全部要打一個你完全無法控制的外部服務。這個外部系統什麼時候會回傳格式跑掉的 XML、什麼時候會逾時、什麼時候會改欄位——這些都不是你能決定的事。

這個組合本身就是測試分層最好的教材:你自己寫的邏輯(Model 關聯、Policy 授權、Job 排程)可以、也應該被綁定在紮實的測試上,一旦壞掉測試就要紅;但跟外部系統交換資料的那一層,測試要驗證的重點不是「這個外部系統回應了什麼」,而是「當外部系統回應長得不如預期時,我這邊的程式碼有沒有正確反應」。

63 支測試檔案,實際分佈長什麼樣

打開這個專案的 tests/ 目錄,會看到兩個資料夾:UnitFeature。乍看之下跟教科書講的一樣,兩層分工。但實際點開檔案數量,會發現一件不太符合直覺的事:

  • tests/Unit/:只有 7 支檔案——2 支測 Model 的純邏輯(accessor、Query Scope),2 支測跟外部校務系統對接的爬取/解析邏輯,2 支測 SOAP 協議層本身,剩下 1 支是框架預設留下的範例測試。
  • tests/Feature/:其餘全部,涵蓋 V1/V2 兩版前台的頁面測試、Model 的整合測試、Filament 後台的 Resource 測試、Console Command 測試、Queue Job 測試。

換句話說,這個系統的 Unit 測試占比非常低,大多數的驗證都發生在 Feature 層。這不是「這個專案沒有把測試分好」,而是反映了一個很實際的判斷:這個系統裡真正「不碰資料庫、不碰外部依賴、純邏輯運算」的程式碼本來就不多,大部分業務邏輯天生就跟 Eloquent 模型、資料庫查詢、甚至外部服務的回應綁在一起,硬要把它們拆成可以獨立測的純函式,反而會讓程式碼結構為了測試而扭曲。

值得先點名的還有 tests/Support/ 這個資料夾,裡面放的不是測試案例本身,而是兩個自製的測試替身:一個模擬 HTTP 客戶端的行為,一個模擬 SOAP 呼叫器的行為。這兩支類別會在後面 Day 16 跟 Day 26 深入拆解——先說結論:它們讓這個系統可以在完全不真的打外部服務的情況下,測試「當外部系統回傳某種特定格式時,我這邊的程式碼會怎麼反應」,這正是前面提到的「測試該驗證什麼」這個問題的具體答案。

一個先預告的落差:覆蓋率數字好看,不代表保護力足夠

這個系列不會只講「這裡的測試設計得很好」,也會誠實揭露測試覆蓋率數字跟實際保護力之間的落差。這裡先舉一個會在 Day 14 跟 Day 19 詳細展開的例子,讓你有個底:

只看測試檔案存不存在

$ ls tests/Feature/V1/LoginTest.php
tests/Feature/V1/LoginTest.php

$ php artisan test --filter=LoginTest
✓ login endpoint exists
1 passed

看起來登入功能有測試覆蓋,而且測試通過。但如果打開這支測試檔案的內容,會發現它只驗證了「這個路由存在、回傳的不是 404」,真正涉及帳號密碼驗證、SSO 登入流程的測試案例,整支被標記成跳過(skip),沒有真的執行。

看測試實際驗證了什麼行為

$ php artisan test --filter=LoginTest -v
✓ login endpoint exists (淺層:只驗證路由可達)
- sso login flow with valid credentials (SKIPPED)
- sso login flow with invalid credentials (SKIPPED)

同一份執行結果,多印出跳過清單之後,畫面完全不一樣——這才看得出來,「登入功能有測試」跟「登入功能真的被測試保護著」,中間有一段沒被填上的落差。

這個系列想傳達的態度是:測試檔案存在、測試綠燈,都只是保護力的必要條件,不是充分條件。真正要問的問題永遠是「如果這裡的邏輯壞了,有沒有一個測試會因此變紅」。

今日思考題

回想你手上維護的專案,有沒有一支測試檔案的名字看起來「應該」涵蓋了某個重要功能,但你已經很久沒有真的打開看過它實際斷言了什麼?如果現在打開來看,你有多少把握它會在那個功能壞掉的時候真的變紅?

今日重點回顧

  • 這個系列的素材是一套真實運作中的大學入口網站:兩版前台、一個 Filament 後台、還要跟外部校務系統做 SOAP 對接
  • 63 支測試檔案裡,Unit 只占 7 支,其餘全部是 Feature——這反映系統裡純邏輯程式碼占比低,多數驗證天生就要碰資料庫或外部依賴
  • tests/Support/ 裡的自製測試替身,讓系統可以在不真的打外部服務的情況下驗證「回應格式跑掉時我這邊會怎麼反應」
  • 用一個登入測試的例子,預告了這個系列會反覆處理的核心命題:測試存在、測試綠燈,不等於真的有保護力

明日預告

明天要講一個這個專案真的發生過的事:從 PHPUnit 遷移到 Pest 之後,留下了什麼痕跡——包括一支看起來還沒遷移完的舊測試檔案,以及同一次遷移裡順便做掉的另一件事:把外部服務錄製重播機制換成自製的測試替身。


延伸閱讀:本次鐵人賽同時並行的其他系列,會從不同角度處理相關的經驗,有興趣可以一起追:


系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊1
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言