「這個頁面回傳 200,測試就算過了嗎?」
這是很多人寫 Feature 測試時,不知不覺會停下來的地方——狀態碼是 200,代表伺服器沒有拋出例外、路由正常運作,聽起來已經是個不錯的訊號。但今天要說的是:「回應成功」跟「回應了正確的內容」之間,還有一段沒被驗證到的距離,而這段距離,往往才是使用者真正在乎的部分。
Feature 測試最基本的用法,是模擬一次真實的 HTTP 請求,檢查伺服器有沒有正常回應:
test('loads successfully', function () {
get('/')
->assertOk();
});
這支測試驗證的事情很單純:首頁這個路由存在、沒有因為程式碼錯誤而拋出例外、回應狀態碼是 200。這一層驗證的價值不能小看——它能在最早期抓出「這支路由整個掛掉了」這種等級的問題,例如某次改動不小心把路由定義刪掉、Controller 建構子拋出例外、View 檔案找不到。這些問題如果沒有測試守著,往往要等到有人真的打開瀏覽器才會發現。
但這層驗證的極限也很明顯:一個回傳 200 的頁面,內容可以是完全空白、可以是顯示了錯誤的資料、可以是漏掉了某個區塊——只要沒有拋出例外,狀態碼永遠是 200。狀態碼驗證不了「這個頁面是不是真的在做它該做的事」。
要驗證「這個頁面真的顯示了我要的資料」,需要更進一步的斷言。這個系統的做法是先準備好測試資料,再檢查回應內容裡有沒有出現這筆資料該有的文字:
test('displays headline news from category', function () {
$news = News::factory()->create([
'news_category_id' => 7,
'title' => '測試用新聞標題',
]);
get('/')
->assertSee($news->title);
});
這支測試多做了一件事:先用 Factory 建立一筆有明確身份(特定分類、特定標題)的測試資料,再檢查首頁回應內容裡有沒有出現這個標題。這一層驗證回答的問題,從「這個頁面能不能被訪問」,變成「這個頁面有沒有正確查出並顯示我要驗證的那批資料」——如果查詢條件寫錯、關聯資料抓錯、View 迴圈邏輯漏掉某個區塊,這支測試都會失敗,而純看狀態碼的測試完全抓不到這些問題。
把這兩種寫法放在一起看,可以整理出一個實用的判斷:
判斷的關鍵問題是:如果這個頁面的核心邏輯壞掉了(查詢條件錯、資料沒顯示、顯示錯資料),狀態碼會不會因此改變?如果答案是不會,那狀態碼驗證對這個情境來說就是不夠的。
test('news list page works', function () {
News::factory()->count(5)->create(['news_category_id' => 1]);
get('/news')
->assertOk();
});
這支測試的名字叫「新聞列表頁能運作」,但它實際驗證的只有「這個路由回應 200」。就算查詢條件寫錯,把所有新聞都篩選掉、畫面上一筆資料都沒顯示,這支測試依然會綠燈通過——測試的名字給了錯誤的安全感。
test('news list displays news from the given category', function () {
$matching = News::factory()->create(['news_category_id' => 1, 'title' => '分類內新聞']);
$other = News::factory()->create(['news_category_id' => 2, 'title' => '其他分類新聞']);
get('/news?category=1')
->assertOk()
->assertSee($matching->title)
->assertDontSee($other->title);
});
這支測試不只驗證狀態碼,還驗證了「符合條件的資料有顯示」跟「不符合條件的資料沒有出現」——後者容易被忽略,但同樣重要:一個篩選功能如果篩選條件完全失效、顯示了所有資料,光看「該顯示的資料有沒有出現」是抓不出來的,只有同時驗證「不該出現的有沒有被排除」,才能真正證明篩選邏輯有效。
回想你手上維護的 Feature 測試套件,有沒有一支測試的名字聽起來驗證了某個具體功能,但實際上只驗證了狀態碼?如果現在幫它補上內容驗證,你有多大把握它會一次就通過?
明天要進到 Filament 後台的測試手法——測後台跟測一般前台頁面的驗證方式不一樣,這篇只講怎麼測,不深入教 Filament 本身怎麼開發。