「授權邏輯這麼重要,應該要有專門的 unit test 才對吧?」
這是我一開始也有的直覺:Policy 決定誰能看什麼、誰能做什麼,理論上應該是整個系統裡最需要被獨立測試保護的一層。但實際去翻這個系統的測試檔案時,發現一件出乎意料的事——完全找不到任何一支直接 new UserPolicy()、直接呼叫 Policy 方法斷言回傳值的測試。今天要講的是:這不是漏測,而是一個關於「該測哪一層才真的有效」的判斷。
打開 app/Policies/UserPolicy.php 這類檔案會發現,裡面幾乎每個方法都只做一件事:把判斷丟給 Spatie Permission 套件的 $user->can('...')。也就是說,Policy 本身沒有自己的業務邏輯,它只是把「這個角色有沒有這個權限」這個問題,轉手交給底層的權限套件去回答。
如果單獨寫一支測試,建立一個 User、指定一個角色、呼叫 $policy->view($user, $model),斷言回傳 true 或 false——這支測試驗證到的東西,其實只是「Spatie Permission 套件本身有沒有正確運作」,而不是這個系統自己寫的業務邏輯有沒有正確。真正屬於這個系統的邏輯,藏在另一個地方:Filament Resource 的 getEloquentQuery() 裡,依角色做的查詢過濾。
❌ 只測 Policy 回傳值
test('admin can view user', function () {
$admin = User::factory()->admin()->create();
$target = User::factory()->create();
expect((new UserPolicy)->view($admin, $target))->toBeTrue();
});
這支測試通過,只證明了「Spatie Permission 套件的 can() 方法運作正常」——如果套件本身有 bug,這支測試也會被連帶測到,但如果系統自己的權限設定表(哪個角色對應哪些權限)設錯了,這支測試完全抓不到,因為它繞過了真實的資料流程,直接手動指定角色。
✅ 測「這個角色在畫面上實際看不看得到」
test('non-admin cannot see super admin in user list', function () {
asAdmin();
$superAdmin = User::factory()->superAdmin()->create();
livewire(UserResource\Pages\ListUsers::class)
->assertCanNotSeeTableRecords([$superAdmin]);
});
這支測試走的是真實路徑:用 asAdmin() 建立一個真的登入使用者、真的透過 Filament Resource 的查詢邏輯(getEloquentQuery() 裡的角色過濾條件)撈資料、驗證畫面上真的看不到不該看到的那筆資料。這支測試同時涵蓋了 Policy 委派、查詢過濾、還有整個授權鏈路串起來之後的實際結果——任何一個環節設錯,這支測試都會紅。
單獨測 Policy 方法的回傳值,測的是「這一小段程式碼有沒有照我寫的邏輯執行」;測 Filament Resource 的實際查詢結果,測的是「使用者最終體驗到的行為對不對」。對授權邏輯來說,後者才是真正重要的問題——使用者不會知道你的 Policy 方法回傳了什麼,他們只會知道自己看不看得到某一筆資料、能不能點某一個按鈕。
這不代表 Policy 永遠不需要獨立測試。如果 Policy 裡真的包含自己的判斷邏輯(不只是單純委派),例如「這個角色只能在工作時間內操作」這種跟權限套件無關的規則,那條邏輯就值得獨立測試——判斷標準是「這段程式碼有沒有屬於這個系統自己的業務邏輯」,而不是「這是不是授權相關的程式碼」。
回想你手上專案裡的授權邏輯:有沒有一段程式碼看起來像是「自己的業務規則」,但拆開來看其實只是在轉手呼叫某個套件的方法?如果把它單獨抽出來測,你測到的到底是自己的邏輯,還是套件本身的行為?
明天要講一次架構重構同時帶動測試結構調整的真實案例——表單跟表格的測試檔案,是怎麼從混在一起變成各自獨立的。