iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

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

Day 04:Model Factory 語意化設計,測試資料怎麼建立才不會互相污染

  • 分享至 

  • xImage
  •  

前言

User::factory()->create() 建出來的使用者,到底是一般人還是管理員?」

如果你的測試套件裡到處都是這種通用的 factory 呼叫,再搭配後面接一串 ->assignRole(...)、手動塞欄位的程式碼,讀測試的人得逐行拼湊才知道這筆測試資料的身份到底是什麼。今天要講的是這個系統怎麼用語意化的 Factory 方法解決這個問題——讓測試資料的建立方式,本身就是一份說明文件。

今日目標

  • 理解語意化 Factory state 方法解決的問題:讓測試資料的身份,從呼叫方式就能一眼看懂
  • 看這個系統怎麼設計 superAdmin()admin() 這兩個具名建構方法
  • 從一段被註解掉的程式碼,學到「什麼時候該直接刪掉、什麼時候該留著當備忘」的判斷
  • 認識這個系統的 Factory 涵蓋範圍,理解「每個 Model 都有對應 Factory」帶來的好處

語意化 state:讓呼叫方式本身就是說明文件

Laravel 的 Factory 機制允許你在基礎的建構邏輯之上,疊加各種「state」方法,每個 state 方法代表一種變形。這個系統的使用者 Factory,就定義了兩個語意清楚的 state:一個叫 superAdmin(),一個叫 admin()

比較這兩種寫法:

// 沒有語意化 state:要讀完才知道這是什麼身份
$user = User::factory()->create();
$user->assignRole('Admin');

// 用語意化 state:呼叫方式本身就是說明
$user = User::factory()->admin()->create();

第二種寫法的好處不只是少打幾行字。當測試檔案裡有幾十處建立測試使用者的地方,呼叫方式本身能不能一眼看出這是什麼角色,直接決定了這份測試日後好不好維護。如果角色指派邏輯散落在每個測試檔案各自手動 assignRole(),日後角色系統一旦調整(例如角色名稱改變、指派方式改變),要修改的地方會散落在整個測試套件裡;集中成一個 Factory state,需要調整時只要改一個地方。

admin() 這個 state 內部用 Laravel Factory 的關聯建構語法,直接把對應的角色關聯一起建出來,呼叫端完全不用知道「建立一個 Admin,背後其實需要先有一個角色紀錄、再建立關聯」這個實作細節——這正是 Factory 該扮演的角色:把「怎麼組出一筆特定身份的測試資料」這件事封裝起來,呼叫端只需要知道「我要一個 Admin」。

一段留著的註解,比刪掉更有價值

翻這支 Factory 的程式碼時,會發現 admin() 方法裡有一段被註解掉、沒有真的執行的程式碼——原本似乎打算讓 Admin 身份同時帶一組更細緻的權限組合,不只是掛一個角色,而是連底層的個別能力也一起指定。

這段程式碼最後被簡化掉了,只留下角色關聯,沒有留下細緻的權限組合。但它沒有被整段刪除,而是用註解的方式留在原地。

這裡有一個值得停下來想的判斷:什麼時候該把沒用到的程式碼直接刪掉,什麼時候該留著當備忘?

如果這段程式碼只是單純寫錯、或是一時想法後來證明是錯誤方向,留著只會造成閱讀干擾,應該直接刪掉——反正版本控制系統會留下歷史紀錄,真的需要回頭查也查得到。

但如果這段程式碼代表的是「一個曾經認真考慮過、但目前判斷不需要的更複雜方案」,留著當註解,等於是在程式碼現場留下一句「這裡曾經想過更細緻的做法,後來簡化了」的提示——對下一個維護者(包括半年後的自己)來說,這比什麼都沒留下,或是只留在某次 code review 討論串裡,更容易在需要時被重新想起來。判斷的關鍵不是「這段程式碼還有沒有用」,而是「這段程式碼背後的思考過程,值不值得被記住」。

❌ 用通用建構方式,角色邏輯散落各處

// 測試檔案 A
$manager = User::factory()->create();
$manager->assignRole('Admin');

// 測試檔案 B(換了一種寫法,但意思一樣)
$user = User::factory()->create(['role' => 'admin']); // 這欄位甚至不一定存在

// 測試檔案 C
$adminUser = User::factory()->create();
Role::create(['name' => 'Admin']);
$adminUser->roles()->attach(...);

三種寫法,做的事情本質相同,但寫法各自不同,而且沒有一種一眼就能看出這是「建一個 Admin」。日後角色指派邏輯要調整,得先找出所有這些散落的寫法。

✅ 語意化 Factory state,集中管理建構邏輯

class UserFactory extends Factory
{
    public function admin(): static
    {
        return $this->has(Role::factory()->state(['name' => 'Admin']));
    }

    public function superAdmin(): static
    {
        return $this->afterCreating(function (User $user) {
            $user->assignRole('Super Admin');
        });
    }
}

// 呼叫端統一、清楚
$manager = User::factory()->admin()->create();
$owner = User::factory()->superAdmin()->create();

角色指派邏輯只存在一個地方,呼叫端不需要知道實作細節,未來調整也只需要改一處。

Factory 涵蓋範圍:14 個 Model,沒有例外

這個系統裡總共 14 個 Model,每一個都有對應的 Factory。這件事本身值得一提——不是每個專案都會堅持「每個 Model 都要有 Factory」,有些團隊只會幫「常在測試裡用到」的 Model 寫 Factory,其他的臨時需要時才手動塞資料。

堅持每個 Model 都有 Factory 的好處,會在測試套件持續成長的過程中慢慢顯現:當你需要一筆新的測試資料時,永遠有一個統一、可預期的入口可以用,不用每次都先確認「這個 Model 到底有沒有 Factory」,也不用因為某個 Model 沒有 Factory,被迫在測試裡手動拼湊建構邏輯,讓那支測試檔案的可讀性跟其他測試不一致。

今日思考題

回想你手上維護的測試套件,有沒有一種身份或狀態,是靠散落各處的手動指派邏輯拼湊出來的?如果把它收斂成一個語意化的 Factory state,除了少打幾行程式碼之外,還會讓哪些測試檔案變得更容易讀懂?

今日重點回顧

  • 語意化 Factory state(例如 admin()superAdmin())讓測試資料的建立方式本身就是說明文件,不用逐行拼湊才知道這筆資料的身份
  • 角色指派邏輯集中在 Factory 裡,日後角色系統調整時只需要改一個地方,不用滿專案搜尋散落的寫法
  • 一段被註解掉、沒有直接刪除的程式碼,可能代表「曾經認真考慮過、後來簡化」的思考過程,值得留下痕跡
  • 每個 Model 都有對應 Factory,讓測試資料的建立方式在整個專案裡保持一致、可預期

明日預告

明天要講一個真實發生過的重構:這個系統原本讓測試資料依賴 migration 裡塞的預設資料,後來為什麼、又是怎麼改成讓每支測試自己用 Factory 建立資料。


上一篇
Day 03:測試輔助函式設計——asAdmin()/asSuperAdmin() 怎麼幫全專案省重複程式碼
下一篇
Day 05:測試資料建置的取捨——migration 內建資料 vs factory 建立
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言