iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

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

Day 10:巢狀資源測試——樹狀編輯介面的獨立模式 vs 內嵌模式怎麼測

  • 分享至 

  • xImage
  •  

前言

「同一個元件,有時候要單獨顯示,有時候要嵌在別的頁面裡顯示,這種情況測試該怎麼寫?」

昨天講的是「怎麼測一個 Filament 元件的動作有沒有真的發生」,今天要往前推一步:如果這個元件本身還帶著「可以獨立運作,也可以被其他頁面內嵌呼叫」這種雙重身份,測試要怎麼同時涵蓋這兩種使用情境?這個系統的頁面樹編輯介面,剛好是這種案例的具體範例。

今日目標

  • 認識「獨立入口」跟「內嵌顯示」這兩種元件使用模式的差異
  • 看這個系統怎麼用一個參數切換元件的顯示模式,測試怎麼分別涵蓋
  • 學到巢狀資料在測試裡的建立方式,以及這個選擇背後的取捨
  • 理解 shouldRegisterNavigation() 這類方法在測試裡代表什麼意義

一個元件,兩種身份

這個系統的頁面管理功能裡,有一個負責顯示樹狀結構的元件——用來呈現頁面之間的父子階層關係,讓管理員可以直覺地看到整個網站的頁面架構。

這個元件有一個特別的設計:它不是一個獨立的後台選單入口。如果你去檢查它有沒有在後台導覽列註冊自己的選單項目,答案是沒有——這個判斷方法回傳的結果明確是「不要」。這代表它的存在意義是被其他頁面呼叫、內嵌顯示,而不是讓使用者直接從選單點進來單獨使用。

同時,這個元件本身接受一個參數,可以決定它現在是以「內嵌」模式運作,還是被單獨掛載測試。這種設計讓同一份元件邏輯可以在不同情境下重複使用,但也意味著測試需要分別驗證這兩種模式底下的行為是否都正確。

巢狀資料怎麼建:一層層手動 create

要測試樹狀結構顯示對不對,測試資料本身就得先是一個真正的多層樹——例如一個根節點,底下有兩個子節點,其中一個子節點底下再有一個孫節點。

這個系統的測試選擇用最直接的方式手動建立這個結構,一層一層透過父節點 ID 串接:

test('displays tree nodes', function () {
    $root = Page::factory()->create(['parent_id' => null, 'name' => '根節點']);
    $child1 = Page::factory()->create(['parent_id' => $root->id, 'name' => '子節點一']);
    $child2 = Page::factory()->create(['parent_id' => $root->id, 'name' => '子節點二']);
    $grandchild = Page::factory()->create(['parent_id' => $child1->id, 'name' => '孫節點']);

    livewire(TreePage::class)
        ->assertSee($root->name)
        ->assertSee($child1->name)
        ->assertSee($child2->name)
        ->assertSee($grandchild->name);
});

這裡有一個值得停下來想的設計選擇:沒有用遞迴的 Factory 語法去自動產生多層結構,而是逐層手動建立,每一層的父節點 ID 都明確指定

這樣寫比較囉唆,但換來的好處是:測試資料的結構完全透明——任何讀這支測試的人,一眼就能看出這個樹長什麼樣子(根節點下兩個子節點,其中一個子節點下還有一個孫節點),不需要去理解某個遞迴 Factory 邏輯背後產生的隨機結構。當測試的重點是「驗證這個特定形狀的樹,顯示結果對不對」,資料結構的可讀性,比建立資料的程式碼精簡度更重要。

如果測試的目的是「驗證這個系統能不能處理任意深度、任意分支數的樹」,遞迴產生大量隨機資料可能更合適;但當測試目的是「驗證特定情境下顯示結果正確」,手動控制每一層的形狀反而是更清楚的選擇。

兩種模式,分別測

回到元件的雙重身份——同一個元件類別,可以用內嵌參數決定顯示模式。測試需要針對這兩種模式分別驗證:

test('displays as embedded component', function () {
    $root = Page::factory()->create();

    livewire(TreePage::class, ['embedded' => true])
        ->assertSee($root->name);
});

test('does not register its own navigation entry', function () {
    expect(TreePage::shouldRegisterNavigation())->toBeFalse();
});

第一支測試驗證「當這個元件以內嵌模式掛載時,依然能正確顯示資料」——因為內嵌模式下,元件可能拿到的參數、渲染的上下文跟獨立掛載時不完全一樣,需要單獨確認這個模式下行為依然正確。

第二支測試驗證的則是一個更宏觀的架構決定:這個頁面元件明確不該出現在後台導覽選單裡。這種測試乍看之下有點抽象——它沒有驗證任何「使用者操作」,只是驗證一個設定方法的回傳值。但它的價值在於:把「這個元件的設計意圖是被內嵌使用,不是獨立入口」這個架構決策,變成一個會被測試套件持續守護的斷言。如果日後有人不小心(或誤解了這個元件的用途)把它註冊成獨立選單項目,這支測試會立刻紅燈,提醒這個改動可能違反了原本的設計意圖。

❌ 只測其中一種模式,假設另一種「應該也沒問題」

test('tree page works', function () {
    $root = Page::factory()->create();

    livewire(TreePage::class)
        ->assertSee($root->name);
});
// 沒有測試 embedded 模式,也沒有驗證導覽註冊行為

這支測試只驗證了元件在預設(獨立掛載)情境下能正常運作,完全沒有涵蓋內嵌模式——如果內嵌模式下有任何跟參數處理相關的邏輯壞掉,這支測試套件不會有任何反應。

✅ 兩種模式跟架構決策,分別驗證

test('displays tree nodes when mounted directly', function () {
    // 驗證獨立掛載模式
});

test('displays as embedded component', function () {
    // 驗證內嵌模式下同樣能正確顯示
});

test('does not register its own navigation entry', function () {
    // 驗證這個架構決策本身沒有被意外改動
});

三支測試各自負責一個獨立的驗證目標,加起來才完整涵蓋了這個元件「顯示邏輯」跟「架構定位」兩個層面。

今日思考題

回想你手上維護的元件或模組,有沒有一個「身兼多種使用情境」的設計,但測試只涵蓋了其中一種?如果另一種情境下的行為壞掉了,現在的測試套件會不會有任何反應?

今日重點回顧

  • 一個 Filament 頁面元件可以身兼「獨立入口」跟「被其他頁面內嵌呼叫」兩種身份,測試需要分別涵蓋這兩種使用模式
  • 巢狀樹狀測試資料選擇逐層手動建立而不是用遞迴 Factory,換來測試資料結構的透明度,適合驗證特定形狀的樹是否顯示正確
  • shouldRegisterNavigation() 這類設定方法的測試,驗證的是架構決策本身,把設計意圖變成測試套件持續守護的斷言
  • 一個元件如果有多種使用情境,只測其中一種等於留下一塊完全沒有安全網的行為範圍

明日預告

明天要看一次真實的權限改動,怎麼被拆成兩支獨立的測試檔案分別驗證不同的面向——一個功能改動不一定要塞進一支巨大的測試檔案裡。


上一篇
Day 09:Filament 後台測試怎麼寫——測「動作有沒有真的發生」而不是「畫面長什麼樣」
下一篇
Day 11:使用者與角色測試——一個權限改動拆成兩支獨立測試檔各自驗證
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言