iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Modern Web

Laravel Filament 從入門到實戰系列 第 25 篇

Day 24:幫 Panel 寫測試,Testable 方法組合怎麼用

  • 分享至 

  • xImage
  •  

Day 23:檔案上傳與資料匯入匯出,正式環境常踩的坑 結尾留下一句話,幫 Panel 寫測試,Testable 方法組合怎麼用,是接下來要處理的內容。今天要處理的正是這句話。

昨天補完商品照片上傳、商品批次匯入、訂單資料匯出這三個功能之後,收尾那段話其實藏著一個不太舒服的事實。這三個功能全部涉及格式驗證、大量資料處理,正是最容易出錯的環節,但驗證它們是否正常運作的方式,從頭到尾只有一種,打開瀏覽器,手動點一遍。上傳一張圖片看縮圖有沒有跑出來,準備一份夾雜錯誤資料的檔案跑一次匯入,點下匯出按鈕再打開下載回來的檔案核對欄位。這件事今天做起來沒問題,程式碼剛寫完,邏輯還新鮮地印在腦子裡,手動點一輪確實能安心。

問題是專案不會停在今天。商品、訂單、客戶、訂單明細這幾個模組彼此關聯,Day 17:多租戶隔離,讓每個經銷據點只看到自己的資料 定案的多租戶隔離跟 Day 16:設計一份貼近真實團隊的角色權限矩陣 定案的角色權限又疊加在上面,修改一處欄位或一段邏輯,很可能不小心影響到另一個原本運作正常的功能。半年後某次調整商品驗證規則,或是替訂單加一個新狀態,改完程式碼要重新安心一次,得把系列前面二十三天做出來的每個功能全部手動點過一輪,這個成本已經高到不切實際。今天要正式引入 Filament 官方提供的測試輔助方法組合,優先替昨天剛補上、風險最高的商品批次匯入功能,寫出第一批自動化測試。

這套方法組合叫 Testable,測的是 Livewire 元件在做什麼

先把今天要用的工具講清楚。Filament 提供了一套 Pest/PHPUnit 測試輔助方法組合,用來對 Panel 內的頁面與元件撰寫自動化測試,這套方法組合可以稱為 Testable。

之所以需要這麼一套專用方法,跟 Filament 頁面的底層實作方式有關。Day 02:安裝與第一個 Panel,認識這個後台入口容器 定案的 Panel,裡面每一個列表頁、建立頁、編輯頁,底層都是一個 Livewire 元件,畫面上的每一次篩選、每一次表單填寫、每一次按鈕點擊,本質上都是元件狀態的更新跟事件的觸發。如果要用一般模擬瀏覽器渲染的方式測試,得先讓測試環境真的把整個頁面渲染出來,再用 CSS 選擇器去找按鈕、填欄位、點擊、等待畫面更新,光是這一層互動細節就相當繁瑣,寫起來像是在隔著一層毛玻璃操作,看不清楚元件內部實際發生了什麼。

Testable 提供的寫法貼近元件互動本身的語意。呼叫 livewire() 掛載一個頁面元件之後,接下來的操作近似「呼叫某個動作、填入某個欄位、斷言某個結果」,例如 fillForm() 填入表單資料,call('create') 觸發建立動作,assertHasNoFormErrors() 斷言沒有驗證錯誤。測試程式碼讀起來像是在描述一次真實的操作流程,而不需要真的處理瀏覽器渲染這一層,也不用煩惱畫面上某個按鈕的 CSS 選擇器改了名字,測試就跟著壞掉。

這裡要先講清楚一件事,Testable 不是用來取代一般 Laravel 測試工具的另一套全新框架,而是在既有的 Pest 或 PHPUnit 測試框架之上,針對 Panel 內頁面與元件這個特定情境,額外提供的一組專用輔助方法。如果測試對象是一個 Controller 或者 Model 本身的邏輯,寫法上跟平常寫 Laravel 測試不會有太大差異,該用 RefreshDatabase、該用 assertDatabaseHas()、該用工廠準備測試資料,這些都照舊,Testable 只是補上了「怎麼跟 Livewire 元件互動」這一段原本很麻煩的環節。

從商品列表跟建立流程開始,練出寫測試的手感

工具介紹完,先從最單純的案例開始練手感,商品 Resource 的列表頁跟建立流程。列表頁要驗證的事情很直接,頁面能不能正常打開,打開之後能不能看到既有的商品資料:

use function Pest\Livewire\livewire;
use App\Filament\Resources\ProductResource;
use App\Filament\Resources\ProductResource\Pages\ListProducts;
use App\Models\Product;

it('可以正常渲染商品列表頁', function () {
    $this->get(ProductResource::getUrl('index'))->assertSuccessful();
});

it('列表頁能看到既有的商品資料', function () {
    $products = Product::factory()->count(3)->create();

    livewire(ListProducts::class)
        ->assertCanSeeTableRecords($products);
});

第一個測試案例只確認路由能正常回應,是最基本的起手式,畫面上如果有東西壞到連渲染都失敗,這個測試會第一個抓出來。第二個案例往前一步,用工廠準備三筆商品資料,再用 assertCanSeeTableRecords() 斷言表格確實顯示了這幾筆資料,不需要自己解析畫面上的 HTML,Testable 直接比對底層資料集合。

建立流程再往前一步,測的不只是頁面打不打得開,而是填完表單送出後,資料是不是真的寫進資料庫:

use App\Filament\Resources\ProductResource\Pages\CreateProduct;

it('可以透過表單建立一筆商品', function () {
    $newProduct = Product::factory()->make();

    livewire(CreateProduct::class)
        ->fillForm([
            'name' => $newProduct->name,
            'price' => $newProduct->price,
            'stock' => $newProduct->stock,
            'category' => $newProduct->category,
        ])
        ->call('create')
        ->assertHasNoFormErrors();

    $this->assertDatabaseHas(Product::class, [
        'name' => $newProduct->name,
        'price' => $newProduct->price,
    ]);
});

fillForm() 對應的欄位名稱,直接沿用 [[Day 04:表單欄位全解析,從文字輸入到日期選擇器]] 定案的商品表單欄位,call('create') 觸發跟畫面上按下建立按鈕相同的動作,assertHasNoFormErrors() 確認送出過程沒有卡在驗證規則上,最後用 assertDatabaseHas() 直接查資料庫,確認資料真的寫進去了,而不只是畫面上看起來像是成功。這類測試比起單純確認頁面能不能打開,更貼近使用者實際會做的事情,也更能保護住「建立商品」這個核心行為。

測試案例的命名值得花一點心思,可以透過表單建立一筆商品 比起 test create product 這種抽象命名,讀起來更像是在描述一段操作情境。半年後回頭打開這份測試檔案,光看名稱就能知道這段程式碼在保護哪個行為,不需要重新讀一遍斷言內容才搞懂目的是什麼。

批次匯入的測試,把 Day 23 的疑慮真正解決

商品列表跟建立流程練完手感,回到今天真正要處理的重點,Day 23:檔案上傳與資料匯入匯出,正式環境常踩的坑 建立的商品批次匯入功能。這個功能涉及檔案解析、逐列驗證、逐列寫入,是系列裡邏輯最複雜、修改頻率也可能最高的一段,安檢閘門不是每個人進出都要脫鞋開包檢查,但風險高的行李理應多檢查一次,批次匯入正是這種需要優先檢查的行李。

先測正常情境,準備一份格式完全正確的檔案,斷言匯入之後資料庫確實新增了對應筆數的商品:

use Illuminate\Http\UploadedFile;
use App\Filament\Resources\ProductResource\Pages\ListProducts;
use App\Models\Product;

it('可以透過檔案批次匯入商品', function () {
    $csv = "name,price,stock,category\n"
        ."濕式狗糧罐頭,45,200,food\n"
        ."貓抓板,180,80,toy\n";

    $file = UploadedFile::fake()->createWithContent('products.csv', $csv);

    livewire(ListProducts::class)
        ->assertTableActionExists('import')
        ->mountTableAction('import')
        ->setTableActionData(['file' => $file])
        ->callMountedTableAction()
        ->assertHasNoTableActionErrors();

    expect(Product::count())->toBe(2);
});

即使 ImportAction 是掛在商品列表頁的頁首,Testable 底層仍把它視為一種表格動作,測試時對應的是 mountTableAction()、setTableActionData()、callMountedTableAction() 這組方法,而不是一般頁面動作的寫法,這是使用這套方法組合時容易搞混的一個細節。整套流程對應到畫面上,就是使用者點開匯入按鈕、選擇檔案、送出,測試程式碼幾乎是逐步翻譯了這個操作過程。

正常情境測完,接著測 Day 23:檔案上傳與資料匯入匯出,正式環境常踩的坑 結尾特別點出的疑慮,格式錯誤的資料是不是真的會被攔下來,而不是悄悄寫進資料庫:

it('格式錯誤的匯入資料會被攔下而不會寫入', function () {
    $csv = "name,price,stock,category\n"
        ."過期優惠券,免費,10,food\n";

    $file = UploadedFile::fake()->createWithContent('products.csv', $csv);

    livewire(ListProducts::class)
        ->mountTableAction('import')
        ->setTableActionData(['file' => $file])
        ->callMountedTableAction();

    expect(Product::where('name', '過期優惠券')->exists())->toBeFalse();
});

這份測試檔案裡,價格欄位刻意填入「免費」這種文字而非數字,對應 ProductImporter 掛在 price 欄位上的 numeric 驗證規則。測試斷言的重點不是介面有沒有跳出錯誤訊息,而是直接查資料庫,確認這筆格式錯誤的資料沒有被寫進 products 資料表,驗證邏輯確實攔下了它,不是讓它悄悄溜進系統。

批次匯入商品測試涵蓋的兩種情境:正確格式資料驗證寫入筆數,錯誤格式資料驗證確實被攔下不寫入

這組測試建立起來之後,價值不是今天看得到,而是未來每一次有人動到 ProductImporter 的程式碼。不管是調整驗證規則、新增一個匯入欄位,還是換一種檔案解析方式,改完程式碼跑一次這組測試,只要還能通過,就有一定把握匯入功能的核心行為,正確資料能寫入、錯誤資料會被攔下,沒有被不小心破壞。這比起每次改完都重新手動準備測試檔案、重新點過一次匯入流程,省下的時間會隨著專案存活的時間越拉越長。

邏輯有測試把關了,正式環境準備好了嗎

回顧今天做的事,替 Panel 頁面與元件撰寫自動化測試的這套方法組合,今天用商品 Resource 的列表頁、建立流程,以及 Day 23 剛建立的批次匯入功能,寫出了第一批測試案例,商品能不能被正確建立、批次匯入的正確資料能不能寫入、錯誤資料能不能被攔下,這幾個行為現在都有測試在背後看著。

得誠實說一句,今天涵蓋的範圍仍然有限。系列走到現在累積了商品、訂單、客戶、訂單明細,加上權限與多租戶這些疊加邏輯,今天不是把這二十三天所有功能一次補齊測試,只示範了幾個具代表性的案例。務實的做法從來不是追求覆蓋率數字好看,而是先照顧邏輯複雜、修改頻繁、出錯代價高的部分,批次匯入符合這三個條件,才被排進今天優先處理的名單,其餘功能該補測試的份量,留給日後隨著改動頻率再逐步補上。

這套後台現在功能有測試把關,Day 22:資料變多之後,表格為什麼開始變慢 也處理過大量資料的情境,看起來已經萬事俱備。但測試驗證的終究是程式邏輯本身對不對,還沒有驗證這套系統擺到一台真正的正式環境伺服器上,環境設定是否也準備妥當。上線前檢查清單,環境變數、快取與佇列,是接下來要處理的內容。


上一篇
Day 23:檔案上傳與資料匯入匯出,正式環境常踩的坑
系列文
Laravel Filament 從入門到實戰 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言