iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

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

Day 03:測試輔助函式設計——asAdmin()/asSuperAdmin() 怎麼幫全專案省重複程式碼

  • 分享至 

  • xImage
  •  

前言

「每支測試都要先登入一個有權限的使用者,是不是寫一個共用函式,直接把使用者塞進去就好?」

聽起來理所當然,但這句話裡藏著一個容易被忽略的陷阱:如果那個「共用函式」為了圖方便,順手把測試使用者的權限開到最大,你的測試套件從那一刻起,驗證的就不是「一般管理員看得到什麼」,而是「上帝視角看得到什麼」。這兩者的差距,通常要等到某天正式環境出現一個「管理員怎麼看不到這個東西」的回報時,才會突然變得很重要。

今天要拆的是這個系統裡兩支測試輔助函式,它們解決的表面問題是「省重複程式碼」,但背後真正的設計判斷是「測試環境要不要跟著遵守最小權限原則」。

今日目標

  • 理解測試輔助函式最容易踩的一個陷阱:為了方便,讓測試身份的權限比實際情境還要大
  • 看這個系統怎麼用 Gate::before() 動態賦權,只放行一組明確的能力清單
  • 認識一個容易被忽略的前置函式:確保「第一個使用者」這種隱性商業邏輯不會干擾測試結果
  • 學到一個判斷習慣:寫測試輔助函式時,先問「這個函式回傳的身份,權限範圍準不準確」

兩支函式,兩種權限範圍

這個系統的測試輔助檔案裡有兩支很常被呼叫的函式:一支模擬一般管理員登入,一支模擬最高權限使用者登入。乍看之下,兩支函式做的事情差不多——都是建立一個使用者、指派角色、回傳一個已登入的測試情境,方便後面的測試直接接著寫斷言。

但實際打開實作,會發現兩支函式的權限設計完全不同層級:

最高權限的那一支,實作相對單純:建立一個使用者,指派一個名字叫「Super Admin」的角色,結束。因為在這個系統的授權設計裡,「Super Admin」角色本身就代表通行無阻,不需要額外設定任何細節。

一般管理員的那一支,做的事情多了一步:它沒有直接把使用者的角色設成能通過任何檢查,而是用 Laravel 授權機制裡的 Gate::before() 動態掛上一段邏輯——只有當使用者角色是「Admin」、而且這次要檢查的能力落在一組明確列出的白名單裡(例如查看列表、查看單筆、建立、更新、刪除、排序、複製、稽核紀錄),才自動放行;不在白名單裡的能力,一律回到正常的授權檢查流程,不會被這個測試輔助函式偷偷開後門。

為什麼不乾脆兩支都用「Super Admin」比較省事

如果你只是想讓測試「能跑過去」,最偷懶的做法確實是兩支函式都建一個萬能身份,反正權限夠大,什麼操作都通得過,測試不會因為權限不夠而莫名其妙失敗。

但這樣做的代價是:你會失去「一般管理員在這個系統裡實際能做什麼、不能做什麼」這件事的測試覆蓋。如果之後有人不小心把某個功能的權限檢查寫錯(例如應該只有 Super Admin 能刪除某筆資料,卻寫成一般 Admin 也能刪),一支只用萬能身份跑的測試套件完全抓不到這個錯誤——因為測試從頭到尾都是用最大權限在操作,任何權限檢查在它面前都是通的。

這個系統的設計選擇是:測試輔助函式本身也要遵守最小權限原則——asAdmin() 給的身份,權限範圍要盡量貼近「真實情境裡一個 Admin 角色實際擁有的能力」,而不是為了讓測試好寫,就把身份的權限無限放大。

❌ 測試輔助函式權限開太大

function asAdmin(): TestCase
{
    $user = User::factory()->createOne();
    $user->assignRole('Super Admin'); // 圖方便,直接給最大權限

    return test()->actingAs($user);
}

看起來能讓所有測試順利跑過,但代價是:任何原本應該只限 Super Admin 才能做的操作,用這個「假裝是 Admin」的身份一樣暢行無阻,測試完全驗證不出權限邊界有沒有寫對。

✅ 用白名單明確限縮權限範圍

function asAdmin(): TestCase
{
    $standardAbilities = [
        'viewAny', 'view', 'create', 'update', 'delete',
        'deleteAny', 'forceDelete', 'forceDeleteAny',
        'reorder', 'replicate', 'audit',
    ];

    Gate::before(function (?User $user, string $ability) use ($standardAbilities) {
        return $user?->hasRole('Admin')
            && in_array($ability, $standardAbilities)
            ? true
            : null; // 不在白名單內,交還給正常授權邏輯判斷
    });

    return test()->actingAs(User::factory()->admin()->createOne());
}

Gate::before() 回傳 null(而不是 false)是這裡的關鍵細節——回傳 null 代表「這個 hook 不表態,交給後面正常的 Policy/Gate 邏輯繼續判斷」,只有明確在白名單內才直接放行。這讓 asAdmin() 建出來的測試身份,真正只在「一般 Admin 該有的能力」範圍內暢通,白名單以外的能力,還是得靠真正的授權邏輯決定通不通過。

一個容易被忽略的前置動作

除了這兩支模擬登入的函式,還有一支更不起眼的輔助函式:確保系統裡至少存在一個使用者。

為什麼需要這個?因為這個系統的授權邏輯裡有一條隱性規則:第一個註冊的使用者自動視為擁有最高權限,這是很多後台系統常見的「初始化引導」設計——系統剛部署、還沒有任何管理員時,總要有一個入口能讓第一個人取得管理權限。

但這條規則放在測試環境裡,會變成一個隱藏的干擾因子:如果測試資料庫目前一個使用者都沒有,你接下來用 User::factory()->createOne() 建立的「應該是一般使用者」的測試資料,可能會因為它剛好是資料庫裡第一筆使用者紀錄,意外被系統判定成最高權限身份,導致測試的行為跟你原本設計的情境不一致。

先確保「至少有一個使用者」存在,等於是先把這條隱性規則的觸發條件排除掉,讓後面建立的測試身份能真正照著你想要的權限層級運作,不會被一個跟這次測試本身無關的初始化邏輯影響結果。

今日思考題

回想你手上維護的測試輔助函式,有沒有一支為了「讓測試好寫」而給了比真實情境更大的權限?如果現在把它的權限收斂到跟真實情境一致,有沒有哪些既有測試會因此開始失敗——那些失敗的測試,會不會正好是原本沒被測到的權限漏洞?

今日重點回顧

  • 測試輔助函式的陷阱:為了方便省事,讓測試身份的權限比真實情境還要大,會讓測試失去驗證權限邊界的能力
  • 這個系統用 Gate::before() 搭配明確白名單,只放行清楚列出的能力,白名單以外交還正常授權邏輯判斷
  • 一個不起眼的前置函式,排除了「第一個使用者自動變成最高權限」這種隱性規則對測試結果的干擾
  • 核心判斷句:測試輔助函式本身也要遵守最小權限原則,不是「能跑過去就好」

明日預告

明天要看這個系統的 Model Factory 怎麼設計語意化的建構方式——superAdmin()admin() 這種一看名字就知道在建什麼身份的寫法,以及一段被刪掉又留下痕跡的程式碼,暗示著什麼樣的設計取捨。


上一篇
Day 02:一次 PHPUnit → Pest 的遷移,殘留了什麼
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言