iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
ChatGPT & Codex

AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發系列 第 12 篇

Day 12|沒有 Unit Test 的祖傳系統,AI 改完我要怎麼知道沒改壞?

  • 分享至 

  • xImage
  •  

前言/情境導入

前幾天,我們一路處理了一個看似只是「流水號改版」的需求。

從三碼舊格式與四碼新格式共存,到多人同時新增時的 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 修改前後的重要行為沒有被破壞?


核心技術解析

沒有 Unit Test,不代表只能「按一按看看」

第一次維護沒有測試的 Legacy System,很容易走向兩個極端。

第一種是:

沒有 Unit Test,所以根本沒辦法驗證。

第二種則是:

程式打得開、畫面按一按沒 Error,應該就可以了。

兩種其實都很危險。

沒有 Unit Test,確實代表我們少了一張重要的安全網。

但這不代表完全無法驗證。

真正需要做的,是先回答一個更基本的問題:

修改之前,系統原本的行為到底是什麼?

如果連修改前的行為都不知道,那修改後看到一個結果,也很難判斷它到底是:

  • 正確的新行為
  • 原本就存在的舊行為
  • AI 不小心改出來的新 Bug

所以我開始把 Legacy System 的修改與驗證流程拆成四個階段:

Baseline
    ↓
Patch
    ↓
Result
    ↓
Regression Check

這四個階段分別回答:

Baseline
修改前,系統實際怎麼運作?

Patch
這次到底改了什麼?

Result
修改後,實際跑出了什麼結果?

Regression Check
那些原本不該改變的行為,有沒有被破壞?

重點不只是「測修改後的程式」。

而是:

先知道修改前發生什麼,再比較修改後到底改變了什麼。


Baseline、Expected Result 與 Invariant 不要混在一起

以前遇到 Bug,我很容易直接把程式丟給 AI:

這段程式有問題,幫我修正。

AI 很快就會給出答案。

但這樣其實少了一個非常重要的步驟:

記錄修改前的行為。

而且實際整理測試案例時,我現在會刻意把三件事情分開:

  • Baseline:修改前已知、已觀察到的系統行為。
  • Expected Result:這次修改完成後,應該得到的新結果。
  • Invariant:這次修改不應該影響、必須繼續成立的既有行為。

例如今天要修改工程變更單的流水號規則:

測試情境 Baseline Expected Result / Invariant
全新案件新增第一筆 無歷史資料 產生第一個合法編號
已有新格式資料 最後合法編號為 0647 下一筆應為 0648
存在三碼舊資料 歷史資料仍保留 舊資料仍可正常查詢
修改舊資料備註 原編號已存在 原編號不可被新規則擋下
兩位使用者同時新增 同一案件可同時操作 不可取得相同編號
新增途中失敗 原資料狀態完整 Rollback 後不能留下半套資料

這裡有一個很重要的差異。

例如:

0647 → 0648

是這次修改後希望得到的 Expected Result。

但:

舊資料仍然可以查詢

則比較接近這次修改必須守住的 Invariant。

所以在修改以前,我不只會問:

這次修改後應該變成什麼?

還會另外問:

這次無論如何都不能改變什麼?

後者,往往才是 Regression Test 真正要守住的東西。


AI 最適合做的,不是替我宣布「測試通過」

這時 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 也跟著改變,就值得繼續追查。


Row Count 是一個很便宜、但很好用的警報器

有些問題甚至不用一開始就逐欄比較。

例如修改前:

SELECT COUNT(*) AS RecordCount
FROM ChangeOrder
WHERE ProjectNo = 'TEST001';

結果:

RecordCount = 15

執行「新增一筆」之後:

RecordCount = 16

至少從筆數這一層來看,結果符合預期。

如果變成:

RecordCount = 17

那就很值得追。

如果需求根本不是新增,而只是修改備註,筆數卻改變了,也一樣有問題。

但 Row Count 不能證明所有資料都正確。

Row Count 比較像煙霧警報器:它很適合提醒「可能有事」,但不能證明「完全沒事」。


ERP 不能只比筆數,還要比 Aggregate

如果是 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

這時候「速度變快」還不夠。

我要的是:

速度變快,而且資料結果沒有改變。


Golden Master:我不知道答案怎麼算,但我知道以前算出什麼

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,不代表底層真的處於同一個交易邊界。


程式碼實作:建立一份最小 Regression Script

如果今天要交付一個涉及流水號與金額的修改,我可以準備一份簡單的 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 Review

做到這裡,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,我可能還無法證明整套祖傳系統完全沒有問題。

但至少每次修改完成後,我希望自己都能清楚地說:

我可以證明我測過什麼,也知道還有什麼沒測。


上一篇
Day 11|新規則上線後,為什麼十年前的舊資料突然不能修改了?
下一篇
Day 13|祖傳系統的金額為什麼突然變成 0?Debug 時先追資料,不要先改公式
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言