「重構程式碼結構的時候,測試也要跟著大改嗎?」
很多時候答案是「不用,測試驗證的是外顯行為,內部怎麼組織不影響測試怎麼寫」。但今天要講一個例外:這個系統有一次把表單跟表格的定義從 Resource 類別裡抽出來,變成獨立的 Schema/Table 類別,這次重構真的讓測試檔案跟著拆成兩支——而且拆開之後的測試手法,也跟以前不一樣了。
在這次重構之前,一個 Resource 類別裡同時塞著表單欄位定義、表格欄位定義、還有各種 action。重構之後,表單定義搬進獨立的 Schema 類別,表格定義搬進獨立的 Table 類別,Resource 本身只剩下組裝跟基本設定。這是一次典型的「單一職責」重構:原本一個類別要同時扛「畫面長什麼樣」跟「畫面怎麼互動」兩種責任,拆開之後每個類別只回答一個問題。
重構完成之後,對應的測試檔案也從一支拆成兩支:PageFormTest.php 只驗證表單的欄位結構,PageTableTest.php 只驗證表格的欄位結構跟排序邏輯。這不是巧合,是因為測試結構本來就該對應到程式碼的職責邊界——當程式碼的職責被拆乾淨,測試自然也會被拆乾淨,如果測試沒有跟著變乾淨,反而是一個「這次重構可能沒有真正拆對邊界」的警訊。
❌ 驗證表單提交後的行為(Day 11 已經看過的手法)
livewire(PageResource\Pages\CreatePage::class)
->fillForm(['name' => '關於我們', 'slug' => 'about'])
->call('create')
->assertHasNoFormErrors();
expect(Page::where('slug', 'about')->exists())->toBeTrue();
這種寫法驗證的是「使用者填完表單、送出之後,系統有沒有正確反應」——測的是整個提交流程跑完之後的最終結果。
✅ 驗證表單結構本身有沒有配置對
livewire(PageResource\Pages\CreatePage::class)
->assertFormFieldExists('name')
->assertFormFieldExists('slug');
$form = $testable->get('form');
expect($form->getComponent('data.template'))
->toBeInstanceOf(Select::class);
這種寫法不送出表單,直接讀取 Livewire component 內部的 form 定義,斷言某個欄位存在、某個欄位是不是預期的元件類型(例如「模板」欄位是不是一個下拉選單而不是文字輸入框)。這驗證的是表單的結構配置本身,跟「填了資料之後會發生什麼事」是完全不同層級的問題——就算表單結構配置錯了(例如某個欄位型別選錯),只驗證提交行為的測試不一定抓得到,因為測試資料本來就是照著開發者以為的結構去填的。
只驗證提交行為,沒辦法保證表單本身的欄位型別、必填規則、選項來源配置對;只驗證表單結構,沒辦法保證使用者真的能順利完成一次操作。這兩種測試分別回答「這個表單長得對不對」跟「這個表單用起來對不對」兩個不同的問題,重構前混在同一支測試檔案裡的時候,容易只顧著測其中一種、漏了另一種;拆開之後,每一支測試檔案的職責範圍變清楚,反而更容易發現「這裡少測了什麼」。
回想你維護的專案裡,有沒有一次重構是把一個大類別拆成幾個各自負責單一職責的小類別?拆完之後,對應的測試檔案有沒有跟著變乾淨?如果測試檔案還是一大坨混在一起,會不會是這次重構的職責邊界其實沒有真正拆對?
明天要講一支被跳過(skip)的登入測試——測試覆蓋率數字看起來很漂亮,但實際保護力可能遠比表面數字弱,這中間的落差是怎麼發生的。