「這兩支測試檔案根本是複製貼上的,程式碼重複這麼多,不用重構嗎?」
寫測試的人多半都被教育過要遵守 DRY 原則,看到重複的程式碼會反射性地想抽共用邏輯。但今天要用一個真實案例,討論一個常被跳過的前置問題:在動手重構之前,先搞清楚這些重複是「意外造成的」,還是「兩邊本來就該長不一樣,只是剛好用了相似的骨架」。
這個系統的前台同時維護兩個介面版本,兩邊都有自己的 sitemap(網站導覽)頁面測試。V1 版本的測試長這樣:
describe('V1 Sitemap', function () {
test('page loads successfully', function () {
Page::factory()->published()->childOf(1)->count(5)->create();
$response = get('/sitemap');
expect($response->status())->toBe(200)->toMatchRoute('sitemap');
});
test('displays page hierarchy', function () {
$page1 = Page::factory()->published()->childOf(1)
->create(['name' => '關於我們']);
$page2 = Page::factory()->published()->childOf($page1->id)
->create(['name' => '我們的團隊']);
Page::fixTree();
$response = get('/sitemap');
$response->assertSee('關於我們');
$response->assertSee('我們的團隊');
});
test('uses v1 view', function () {
$response = get('/sitemap');
$response->assertViewIs('sitemap');
});
});
V2 版本則是這樣:
describe('V2 Sitemap', function () {
test('page loads successfully', function () {
$parent = Page::factory()->published()->childOf(1)
->create(['name' => '關於我們', 'slug' => 'about']);
Page::factory()->published()->childOf($parent->id)
->create(['name' => '網站簡介', 'slug' => 'intro']);
Page::fixTree();
$response = get('/sitemap');
$response->assertOk();
$response->assertSee('網站導覽');
});
test('displays page tree', function () {
$parent = Page::factory()->published()->childOf(1)
->create(['name' => '關於我們', 'slug' => 'about']);
Page::factory()->published()->childOf($parent->id)
->create(['name' => '網站簡介', 'slug' => 'intro']);
Page::fixTree();
$response = get('/sitemap');
$response->assertSee('關於我們');
$response->assertSee('網站簡介');
});
});
乍看之下兩支測試檔案結構幾乎一致:都用 describe() 包住幾個 test(),都建立頁面資料、都呼叫同一個 /sitemap 路由。但仔細比對斷言內容,會發現差異其實不小:
toMatchRoute('sitemap') 驗證路由,V2 直接用 assertOk()
assertViewIs('sitemap')(確認用的是哪個 view 檔案),V2 完全沒有這個測試兩支測試檔案「看起來很像」是因為它們測的是同一個功能(sitemap 頁面),但「像」的地方停留在骨架層級——真正的斷言內容,是各自根據 V1、V2 介面版本的實際行為差異寫出來的,不是無腦複製貼上就結束。
這個系統目前的選擇是:不抽。兩支測試檔案完全獨立,沒有共用的 helper 或 shared example。這不是疏忽,而是可以理解的取捨——V1、V2 兩套介面版型本來就不一樣(連拿到的 view、驗證的細節都不同),如果硬要抽一層共用邏輯,很可能只能抽出「建立頁面資料」這種最外層、最不重要的部分,真正核心的斷言邏輯——因為兩邊行為本來就不同——還是得各自寫。過度抽象化「表面上相似」的測試,反而可能讓每支測試都要多繞一層去理解共用邏輯在幹嘛,讀起來比兩支各自獨立、一眼看穿在測什麼的版本更難懂。
這不代表「永遠不該抽共用邏輯」——如果兩套版本的核心驗證邏輯本身完全一致,只是套用在不同路由上,抽出共用的 shared example 就會是合理的選擇。判斷的關鍵不在「程式碼看起來像不像」,而在「兩邊在驗證的『行為』本身,是不是真的同一件事」。
面對「要不要抽共用測試邏輯」這個問題,可以先問自己兩個問題:
回想你維護的專案裡,有沒有兩支測試檔案因為「看起來很像」被你(或別人)強行抽成共用邏輯,結果讀起來反而比兩支各自獨立的版本更難懂?
明天要講一個更值得注意的測試覆蓋盲點:一個曾經真的出過 Content-Type bug、線上事故等級的路由,反而找不到對應的 Feature 測試直接驗證它。