iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

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

Day 22:拿掉一支「假」的 unit test——為什麼真實整合測試才可信

  • 分享至 

  • xImage
  •  

前言

「這支測試已經綠燈很久了,應該沒問題吧?」

這句話最危險的地方在於,它把「測試綠燈」直接等同於「這段邏輯真的被驗證過」。今天要拆一個真實發生過的案例:一支存在了一段時間、每次跑都綠燈的 unit test,其實從來沒有真的驗證過它宣稱在測的東西。找出這件事之後,怎麼把它換成一支真的會保護你的測試。

今日目標

  • 看清楚「假 unit test」具體長什麼樣:不是語法錯誤,是資料造得不夠真實
  • 理解「手動塞資料庫紀錄」跟「真的走過完整流程」之間,測試保護力的差距在哪
  • 學會用 config() 切換套件行為,在測試裡重現「舊資料」的真實起始狀態
  • 看到重構前後兩版測試的具體 diff,而不是只聽結論

一支測試,兩個版本

這個系統用 spatie/laravel-media-library 管理檔案上傳,早期檔案路徑是套件預設的 {media_id}/檔名,後來改成自訂的 {model類型}/{model_id}/{media_id}/檔名(方便直接從路徑看出這個檔案屬於哪個 Model、哪一筆資料)。因為既有正式環境的檔案還停留在舊路徑,需要一支遷移指令把它們搬到新格式。

第一版的測試長這樣(節錄核心邏輯):

function createMediaRecord(string $modelType, int $modelId, int $mediaId): Media
{
    return Media::forceCreate([
        'id' => $mediaId,
        'model_type' => $modelType,
        'model_id' => $modelId,
        'collection_name' => 'default',
        'file_name' => 'test.jpg',
        // ...其餘欄位手動填齊
    ]);
}

test('dry-run shows plan without moving files', function () {
    $media = createMediaRecord('App\\Models\\Banner', 1, 100);
    Storage::disk('public')->put('100/test.jpg', 'content');

    $this->artisan('media:migrate-paths', ['--dry-run' => true])
        ->expectsOutputToContain('Would move')
        ->assertSuccessful();
});

這支測試會通過。但它從頭到尾沒有真的呼叫過 addMedia(),沒有真的觸發 Media Library 套件的檔案上傳流程,資料庫裡那筆 Media 紀錄是用 forceCreate() 手動塞出來的假資料。它驗證的只有「遷移指令看到資料庫裡有一筆 model_type/model_id/id 符合條件的紀錄時,會不會印出對的訊息」——這件事本身沒錯,但完全沒有驗證過「套件真的用舊版 PathGenerator 產生檔案時,路徑格式是不是真的長這樣」。

真正的問題出在哪

Media Library 套件的路徑產生邏輯是套件內部實作,不是這個專案自己寫的字串組合。手動塞一筆 model_type/model_id/id 符合條件的資料庫紀錄,只能驗證「遷移指令的查詢條件寫對了」,沒辦法驗證「套件真的用舊版 PathGenerator 產生檔案時,路徑格式是不是真的長這樣」。如果套件本身的路徑格式改了,或者這個專案對套件配置的理解本來就有落差,這支測試完全不會發現。

測試通過,不等於它驗證了你以為它在驗證的事——尤其當測試資料是手動塞出來的,而不是真的走過一次完整流程。

❌ 手動塞資料庫紀錄,繞過真實流程

function createMediaRecord(string $modelType, int $modelId, int $mediaId): Media
{
    return Media::forceCreate([
        'id' => $mediaId,
        'model_type' => $modelType,
        'model_id' => $modelId,
        // 手動填齊套件要求的每一個欄位
        'collection_name' => 'default',
        'file_name' => 'test.jpg',
        'mime_type' => 'image/jpeg',
        'disk' => 'public',
        // ...
    ]);
}

問題:這支輔助函式假設「套件的資料庫紀錄長什麼樣」,卻從來沒有驗證過這個假設是不是真的正確——它繞過了套件本身的邏輯,直接偽造了套件本該產生的結果。

✅ 切換套件設定,真的走一次上傳流程

function uploadWithDefaultGenerator(callable $fn): void
{
    config(['media-library.path_generator' => DefaultPathGenerator::class]);
    app()->forgetInstance('path_generator');
    $fn();
    config(['media-library.path_generator' => ModelPathGenerator::class]);
}

test('dry-run shows plan without moving files', function () {
    uploadWithDefaultGenerator(function () {
        $banner = Banner::factory()->create();
        $banner->addMedia(UploadedFile::fake()->image('hero.jpg'))
            ->toMediaCollection('default');

        $this->test_media = $banner->getFirstMedia();
    });

    $oldPath = "{$this->test_media->id}/hero.jpg";
    Storage::disk('public')->assertExists($oldPath);

    $this->artisan('media:migrate-paths', ['--dry-run' => true])
        ->expectsOutputToContain('Would move')
        ->assertSuccessful();

    // dry-run 不該真的搬動檔案:舊路徑還在,新路徑還沒出現
    Storage::disk('public')->assertExists($oldPath);
});

這個版本先把套件的路徑產生規則暫時切回原本的舊版設定,真的呼叫 addMedia()->toMediaCollection() 觸發套件本身的上傳邏輯,讓套件自己產生一筆「舊路徑」的真實檔案跟資料庫紀錄——這才是一個「真的需要被遷移」的起始狀態。驗證方式也從「指令有沒有印出對的字」,換成「磁碟上的檔案有沒有真的還在舊路徑、還沒被搬到新路徑」,直接檢查系統的實際狀態,而不是只看指令的輸出文字。

為什麼要多寫一個輔助函式去「切換套件設定」

有讀者可能會問:為什麼不能一開始就用新版 ModelPathGenerator 上傳,測「已經是新格式的檔案不會被誤搬」就好?因為這支指令要解決的問題,本質是「處理歷史遺留的舊格式資料」——如果測試資料從一開始就是新格式,等於跳過了這支指令存在的理由。uploadWithDefaultGenerator() 這個輔助函式,就是為了在測試裡精確重現「正式環境裡那些還沒被遷移的舊檔案」長什麼樣,上傳完再把設定切回來,確保後續測試不會被污染。

今日思考題

回想你維護的專案裡,有沒有測試是用「手動塞一筆資料庫紀錄」的方式,去模擬某個套件或某段邏輯本該產生的結果?如果那個套件的行為改變了,你的測試會不會完全沒感覺?

今日重點回顧

  • 一支「看起來在測遷移邏輯」的測試,實際上手動偽造了套件該產生的資料,完全沒驗證過套件本身的行為
  • 測試綠燈只代表「跟測試資料一致的邏輯是對的」,不代表「測試資料本身真實反映了系統會遇到的情況」
  • config() 切換套件行為、真的走一次上傳流程,才能重現「正式環境的舊格式資料」這個起始狀態
  • 驗證方式從「指令印出什麼字」換成「磁碟上的檔案實際狀態」,更貼近真正要保護的行為

明日預告

明天要看跟今天很像、但層次不同的測試:不牽扯遷移指令本身,只驗證「這個 Model、這一筆資料,上傳檔案後該產生什麼路徑」——把一個小單位的行為先確定下來,再去測包住它的整個流程。


上一篇
Day 21:XML 容錯測試——用症狀驗證,不用內部實作細節驗證
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言