前幾天,我們一路處理了一個看似只是「流水號改版」的需求。
從三碼舊格式與四碼新格式共存,到多人同時新增時的 Race Condition,再到規則究竟應該放在 UI、Stored Procedure 還是 Trigger,最後甚至碰到了新規則上線後,十年前的舊資料反而不能修改的 Backward Compatibility 問題。
到了這裡,程式終於改完了。
AI 看過。
SQL 跑得動。
Delphi 也編譯成功。
新增一筆資料,看起來也正常。
那是不是可以交付了?
還不行。
因為 Legacy System 最麻煩的地方,往往不是「新功能能不能動」,而是:
你改好的這個地方,有沒有偷偷讓原本正常的地方壞掉?
如果今天維護的是一套測試完整的現代系統,也許可以直接跑 Unit Test、Integration Test,甚至 CI Pipeline。
但我面對的是一套維護多年的 Delphi Legacy System。
沒有完整的 Unit Test。
沒有自動化的 Regression Test。
甚至很多商業規則,都不是寫在規格文件裡,而是藏在事件、Dataset、Stored Procedure、Trigger,以及使用者多年累積下來的操作流程裡。
這時候 AI 就算告訴我:
「修改已完成,邏輯看起來沒有問題。」
我還是不能直接相信它。
因為 AI 能幫我 Review 程式碼,卻不知道這套系統十年前到底允許使用者怎麼操作。
所以今天真正要處理的問題不是:
沒有 Unit Test,要怎麼測試?
而是:
在沒有 Unit Test 的 Legacy System 裡,我要怎麼建立足夠的證據,證明 AI 修改前後的重要行為沒有被破壞?
第一次維護沒有測試的 Legacy System,很容易走向兩個極端。
第一種是:
沒有 Unit Test,所以根本沒辦法驗證。
第二種則是:
程式打得開、畫面按一按沒 Error,應該就可以了。
兩種其實都很危險。
沒有 Unit Test,確實代表我們少了一張重要的安全網。
但這不代表完全無法驗證。
真正需要做的,是先回答一個更基本的問題:
修改之前,系統原本的行為到底是什麼?
如果連修改前的行為都不知道,那修改後看到一個結果,也很難判斷它到底是:
所以我開始把 Legacy System 的修改與驗證流程拆成四個階段:
Baseline
↓
Patch
↓
Result
↓
Regression Check
這四個階段分別回答:
Baseline
修改前,系統實際怎麼運作?
Patch
這次到底改了什麼?
Result
修改後,實際跑出了什麼結果?
Regression Check
那些原本不該改變的行為,有沒有被破壞?
重點不只是「測修改後的程式」。
而是:
先知道修改前發生什麼,再比較修改後到底改變了什麼。
以前遇到 Bug,我很容易直接把程式丟給 AI:
這段程式有問題,幫我修正。
AI 很快就會給出答案。
但這樣其實少了一個非常重要的步驟:
記錄修改前的行為。
而且實際整理測試案例時,我現在會刻意把三件事情分開:
例如今天要修改工程變更單的流水號規則:
| 測試情境 | Baseline | Expected Result / Invariant |
|---|---|---|
| 全新案件新增第一筆 | 無歷史資料 | 產生第一個合法編號 |
| 已有新格式資料 | 最後合法編號為 0647 |
下一筆應為 0648 |
| 存在三碼舊資料 | 歷史資料仍保留 | 舊資料仍可正常查詢 |
| 修改舊資料備註 | 原編號已存在 | 原編號不可被新規則擋下 |
| 兩位使用者同時新增 | 同一案件可同時操作 | 不可取得相同編號 |
| 新增途中失敗 | 原資料狀態完整 | Rollback 後不能留下半套資料 |
這裡有一個很重要的差異。
例如:
0647 → 0648
是這次修改後希望得到的 Expected Result。
但:
舊資料仍然可以查詢
則比較接近這次修改必須守住的 Invariant。
所以在修改以前,我不只會問:
這次修改後應該變成什麼?
還會另外問:
這次無論如何都不能改變什麼?
後者,往往才是 Regression Test 真正要守住的東西。
這時 AI 可以發揮很大的作用。
但我不會只問:
這段修改有沒有問題?
因為這個問題太寬。
AI 很可能只從目前看到的程式碼判斷。
我現在反而會把需求、Baseline 與 Patch 一起提供:
以下是這次修改前已確認的系統行為,以及本次程式碼修改。
請不要直接判斷修改正確。
請根據修改內容列出可能受到影響的既有流程,
並整理 Regression Test Checklist。
請至少檢查:
1. 新資料
2. 舊資料
3. INSERT
4. UPDATE
5. 查詢
6. 失敗/Rollback
7. 多使用者操作
8. 主檔與明細資料一致性
如果某項無法只靠程式碼判斷,請標示「需要實際驗證」。
我不是要求 AI:
幫我證明程式沒有 Bug。
而是要求它:
幫我找出「我可能忘記測什麼」。
AI 在這個角色上,比較可靠。
Legacy System 還有一個很容易踩到的坑:
只驗證 UI。
例如按下「儲存」之後:
沒有 Exception
→ 畫面顯示成功
→ 關閉視窗
→ 測試完成
看起來很合理。
但如果這次修改涉及資料庫,我真正想確認的至少還包括:
UI 顯示結果
↓
Dataset 狀態
↓
實際 SQL Server 資料
↓
相關主檔/明細
↓
重新查詢後的結果
畫面成功,只能證明畫面沒有立刻爆炸。
它不能直接證明資料正確。
以這次流水號改版為例,測試前可以先記錄指定案件目前的資料。
以下使用匿名化後的名稱示意:
-- 測試前:保存目前資料狀態
SELECT
ProjectNo,
ChangeNo,
Status,
Amount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001'
ORDER BY ChangeNo;
執行 Delphi 的新增或修改流程之後,再執行一次相同查詢:
-- 測試後:重新確認實際寫入資料
SELECT
ProjectNo,
ChangeNo,
Status,
Amount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001'
ORDER BY ChangeNo;
我要看的不只是:
0648 有沒有新增成功?
還要確認:
原本的 0647 有沒有被修改?
舊資料有沒有消失?
筆數是不是只增加 1?
Status 有沒有被意外改變?
Amount 有沒有跟著變動?
這其實是一種很簡單的 Snapshot 思維。
這裡說的 Snapshot,不是 SQL Server 的 Snapshot Isolation 或 Database Snapshot,而只是把修改前後的資料狀態保存下來做比較。
假設修改前是:
A
B
C
需求只是新增 D,那修改後應該是:
A
B
C
D
如果 A、B、C 也跟著改變,就值得繼續追查。
有些問題甚至不用一開始就逐欄比較。
例如修改前:
SELECT COUNT(*) AS RecordCount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001';
結果:
RecordCount = 15
執行「新增一筆」之後:
RecordCount = 16
至少從筆數這一層來看,結果符合預期。
如果變成:
RecordCount = 17
那就很值得追。
如果需求根本不是新增,而只是修改備註,筆數卻改變了,也一樣有問題。
但 Row Count 不能證明所有資料都正確。
Row Count 比較像煙霧警報器:它很適合提醒「可能有事」,但不能證明「完全沒事」。
如果是 ERP,筆數正確也不代表資料正確。
尤其遇到:
數量
單價
複價
追加減金額
合約總額
只要其中一個欄位被算錯,畫面可能還是完全正常。
所以除了 Row Count,我還會增加 Aggregate。
例如:
SELECT
COUNT(*) AS RecordCount,
SUM(Amount) AS TotalAmount
FROM ChangeOrderDetail
WHERE ProjectNo = 'TEST001';
假設修改前:
RecordCount = 80
TotalAmount = 1,250,000
如果這次只是改善效能,商業邏輯根本沒有要求改金額,那修改後就應該特別確認:
RecordCount = 80
TotalAmount = 1,250,000
這時候「速度變快」還不夠。
我要的是:
速度變快,而且資料結果沒有改變。
Legacy System 還有一種很麻煩的情況。
你可能知道某個功能用了十幾年,使用者也認為目前結果是對的。
但是程式裡面有:
大量 if
Dataset Locate
Filter
逐筆計算
跨資料表查詢
特殊客戶條件
你根本沒有把握能在短時間內重新推導所有商業規則。
這時可以採用一種很實用的思維:
Golden Master。
簡單來說:
我暫時不重新證明舊程式為什麼得到這個答案,而是先保存它目前的輸出,修改之後再比較結果。
例如一份報表原本輸出:
原合約金額:1,000,000
本次追加: 50,000
本次追減: 20,000
變更後金額:1,030,000
如果這次修改的只是:
Excel 匯出效能
那修改前後輸出的金額就應該一致。
這種做法本質上也很接近 Legacy Code 常見的 Characterization Test:先把系統「現在實際怎麼做」記錄下來,再保護這些已知行為。
但這裡仍然有一個限制:
Golden Master 只能證明「沒有改變既有行為」,不能證明既有行為本身一定正確。
如果舊程式本來就算錯,Golden Master 也會把錯誤一起保存下來。
所以使用它之前,仍然需要確認:
這個 Baseline 是目前已知且被接受的結果嗎?
Day 11 才剛遇到一個非常典型的問題。
新規則上線之後,新資料新增正常。
結果使用者打開十年前的舊資料,只是想修改一個跟流水號無關的欄位,卻被新的驗證規則擋住。
這種 Bug 如果只測:
新增新資料
永遠測不出來。
所以 Legacy System 有一個很重要的測試資產:
歷史資料真正出現過的「形狀」。
這裡的「形狀」指的不是資料內容本身,而是歷史資料曾經出現過的結構與邊界條件,例如三碼舊編號、缺號、Null、已簽核狀態、大量明細等。
這也不是指直接拿正式環境亂測,而是從歷史資料裡找出有代表性的資料型態,在適當的測試環境建立對應案例。
例如:
| 資料類型 | 驗證目的 |
|---|---|
| 三碼舊編號 | 舊格式是否仍能讀取 |
| 四碼新編號 | 新規則是否正常 |
| 缺號資料 | 新規則是否錯誤假設一定連號 |
| 已簽核資料 | 修改限制是否維持 |
| 空值資料 | Null/空字串處理是否改變 |
| 大量明細 | 效能修改是否改變計算結果 |
這些歷史資料,其實也是 Legacy System 的另一種「測試文件」。
因為它們記錄了:
這套系統過去真的曾經接受過哪些狀態。
看到這裡,可能會有一個疑問:
所以最後還不是人工測?
是。
但「人工測試」跟「隨便按按看」是兩回事。
我現在會建立固定的驗證狀態:
[PASS] 已執行,而且結果符合預期
[FAIL] 已執行,但結果不符合預期
[TODO] 尚未執行
[N/A] 本次修改不適用
接著才是測試項目:
[TODO] 正常新增一筆資料
[TODO] 儲存後重新查詢
[TODO] 修改既有新資料
[TODO] 查詢歷史舊資料
[TODO] 修改允許變更的舊資料欄位
[TODO] 驗證主檔資料
[TODO] 驗證明細資料
[TODO] 比對 Row Count
[TODO] 比對關鍵金額 Aggregate
[TODO] 測試空值/特殊資料
[TODO] 測試操作失敗
[TODO] 確認 Rollback 後沒有殘留資料
[TODO] 涉及產號時測試多人操作
[TODO] 涉及簽核時測試簽核狀態
[TODO] 涉及報表時比對修改前後輸出
[TODO] 關閉畫面重新進入再查一次
這比單純使用 [ ] 多了一個很重要的資訊:
沒勾,到底代表沒測、測失敗,還是根本不適用?
Checklist 真正的價值,就是把工程師腦中的測試經驗,轉成另一個人也能重複執行的驗證流程。
Legacy Delphi 系統還有一個特別需要注意的地方。
畫面上的 Dataset 狀態,不一定等於資料庫最後真正保存的狀態。
所以我不會在看到:
ShowMessage('存檔完成');
之後就結束測試。
而是會:
新增
↓
儲存
↓
關閉/重新查詢
↓
重新定位該筆資料
↓
確認畫面結果
↓
再查 SQL Server
因為這可以抓到很多只看當下畫面看不到的問題,例如:
Post 成功,但 ApplyUpdates 失敗
或者:
畫面上的計算欄位正確,
但實際寫入資料庫的值不正確
甚至 Dataset 還保留著記憶體中的值,重新 Open 後才發現資料根本沒有保存。
所以:
重新查詢不是多做一次,它是在驗證 Persistence。
還有一類測試非常容易被忽略:
Failure Path。
假設這次 AI 幫我修改了一段 Transaction:
Database.StartTransaction;
try
SaveMaster;
SaveDetail;
Database.Commit;
except
Database.Rollback;
raise;
end;
只測正常儲存,只能證明:
成功時可以成功。
但 Transaction 真正重要的地方,其實是:
失敗時會不會留下半套資料?
所以我會刻意建立一個會失敗的測試條件:
主檔成功
↓
明細故意製造錯誤
↓
觸發 Exception
↓
Rollback
↓
重新查詢資料庫
最後確認:
主檔沒有偷偷留下
明細沒有留下部分資料
流水號狀態沒有異常
相關金額沒有被更新一半
但這裡還有一個很重要的前提:
這些寫入真的必須處在同一個 Transaction Boundary 裡。
如果主檔、明細、Stored Procedure 或跨資料庫操作實際使用不同 Connection,就不能只看到程式裡出現 Rollback,便假設所有資料一定會一起復原。
這也是 Legacy System 很容易讓人誤判的地方:
程式碼看起來包在同一段 try...except,不代表底層真的處於同一個交易邊界。
如果今天要交付一個涉及流水號與金額的修改,我可以準備一份簡單的 SQL Script。
-- ============================================
-- Regression Check
-- 測試案件:TEST001
-- ============================================
-- 1. 確認資料筆數
SELECT COUNT(*) AS RecordCount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001';
-- 2. 確認目前案件的流水號與狀態
-- 不直接使用 MAX(ChangeNo),避免混合歷史格式時造成誤判
SELECT
ChangeNo,
Status,
Amount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001'
-- ORDER BY 僅方便查看資料,
-- 不以此排序結果推導下一個流水號
ORDER BY ChangeNo;
-- 3. 確認關鍵金額
SELECT
COUNT(*) AS DetailCount,
SUM(Amount) AS TotalAmount
FROM ChangeOrderDetail
WHERE ProjectNo = 'TEST001';
這裡我刻意沒有使用:
MAX(ChangeNo)
因為前幾天才處理過三碼舊格式、十六進位與四碼十進位混存的問題。
即使這裡只是拿來驗證,而不是拿來產號,直接對混合格式的字串欄位使用 MAX(),仍然很容易讓測試邏輯重新掉回原本的陷阱。
所以測試案例應該直接記錄:
Expected Result:
本次新增 ChangeNo = 0648
然後確認 0648 是否真的存在,而不是重新發明另一套「最大號碼推導規則」。
假設修改前記錄:
Baseline
RecordCount = 15
DetailCount = 80
TotalAmount = 1,250,000
執行修改後流程,再記錄:
Result
RecordCount = 16
新增 ChangeNo = 0648
DetailCount = 80
TotalAmount = 1,250,000
接著進行 Regression Check:
[PASS] 新增筆數符合預期
[PASS] 新流水號為 0648
[PASS] 原有明細筆數沒有改變
[PASS] 原有總金額沒有改變
[TODO] 多使用者同時新增
[TODO] 舊資料修改
[N/A] 本次不涉及報表
到這裡,我才有比較具體的證據可以描述目前驗證到哪裡。
而不是只有:
「我按過了,看起來正常。」
做到這裡,AI 還可以再幫一次忙。
但我不會問:
幫我確認這個修改是不是正確。
而會把四個階段一起交給它:
以下是:
1. Baseline:修改前已確認的系統行為
2. Patch:本次程式碼 Diff
3. Result:修改後實際執行結果
4. Regression Check:目前測試狀態
請不要假設沒有出現 Exception 就代表正確。
請幫我檢查:
- 哪些需求已經有測試證據支持?
- 哪些只能證明 Happy Path?
- 哪些 Regression Risk 還沒有被覆蓋?
- 是否缺少舊資料、空值、失敗流程或多人操作測試?
- 哪些結論目前仍然只能標示為「尚未驗證」?
不要替未執行的測試宣告 PASS。
這時 AI 的角色就不是:
AI 幫我測試
而是:
AI 幫我擴大測試思考範圍
↓
我判斷風險是否適用
↓
轉成可執行測試
↓
取得實際結果
這個差異很重要。
Unit Test 當然很好。
如果今天能重新設計系統,我也希望重要的 Business Logic 都有自動化測試。
但 Legacy System 的現實通常不是:
今天開始補完所有 Unit Test
而是:
下午要修 Bug
明天使用者要測
這週可能就要上線
所以第一步不一定是追求完美的 Testing Architecture。
反而可以先從:
固定測試資料
固定操作步驟
固定 SQL 查詢
固定預期結果
固定 Regression Checklist
開始。
只要一個測試可以:
今天執行一次
下個月再執行一次
另一位工程師也能照著執行
它就已經比:
「我記得上次好像這樣按沒問題。」
可靠很多。
而這些東西未來也有機會慢慢演進:
手動 Checklist
↓
SQL 驗證 Script
↓
Golden Master / Characterization Test
↓
Integration Test
↓
Automated Regression Test
不需要第一天就把祖傳系統改造成現代測試天堂。
先讓它變得可以被驗證,已經是一個很大的進步。
沒有 Unit Test 的祖傳系統,確實比現代系統更難安全修改。
但真正危險的不是只有「沒有 Unit Test」。
而是:
修改前不知道原本行為,修改後又沒有留下任何可以比較的證據。
所以我現在面對 AI 產生的修改,會盡量留下四個階段:
Baseline
修改前實際發生什麼
↓
Patch
這次到底改了什麼
↓
Result
修改後實際發生什麼
↓
Regression Check
原本不該改變的東西,有沒有被破壞
AI 最大的價值,不是替工程師保證:
「這次修改一定沒問題。」
而是幫工程師更快找到:
「還有哪些地方沒有被證明沒問題。」
程式能 Compile,只代表語法過關。
畫面沒有 Error,只代表某條操作路徑走得完。
AI 說 Looks Good,也只代表它目前沒有從提供的 Context 裡看到明顯問題。
沒有 Unit Test,我可能還無法證明整套祖傳系統完全沒有問題。
但至少每次修改完成後,我希望自己都能清楚地說:
我可以證明我測過什麼,也知道還有什麼沒測。