iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
AI Engineering

AI 寫的測試,誰來測?系列 第 29 篇

【Day27】誰來查那個檢查:把「兩邊是不是同一個東西」寫成工具

  • 分享至 

  • xImage
  •  

TL;DR

  • 昨天那個檢查我做錯的不是我以為的地方:不是漏查了幾條,是只查了一對欄位——那對還是我碰巧注意到的
  • 工具掃十份 AI 產物的 232 條斷言,標記 14 條,十份全中,同一件事五種寫法
  • 工具第一版漏了一條,而用來偵測漏網的守衛,設計本身是錯的——它比的兩個數量不同類,相等只是巧合

https://ithelp.ithome.com.tw/upload/images/20260928/20103826VR7lYj680C.jpg


前言

昨天結尾問:143 條關係,一條一條人工查得完嗎?

先講為什麼要把它寫成工具。不是因為慢——grep 一次三十秒。

是因為那份測試不是你寫的,而且你沒有作者可以問。同事寫的你可以走過去問一句「你知道這兩個欄位是同一個物件嗎」,模型不行。它交出檔案就走了,剩下的只有程式碼。

沒有人可以問的時候,就得有個東西替你問。


昨天做錯的不是我以為的地方

寫工具之前先修正一件事。

昨天我以為自己查過了,實際上只查了一對欄位——圖表餘額對真實餘額,因為那是我碰巧注意到的那一對。我從來沒問過:這支程式還有哪些欄位對是親戚?

工具第一層就在回答這個,而且跟測試無關:跑受測物 200 組隨機輸入,逐對檢查輸出欄位之間有沒有恆定關係。

親緣表|受測物 shadow/calc_fixed.py|200 組輸入|種子 20260928

  balances_charted、balances_raw                  IDENTITY
  projected_savings、retirement_gap、target_fund   LINEAR

第二組我昨天連想都沒想過。

兩種關係的證據強度不一樣,報表要分開:

  • IDENTITY 是 is 判定,確鑿
  • LINEAR 是在這 200 組輸入上成立,那是推論不是證明,換一組輸入可能就不成立

所以工具的價值不在找得比較全。同一個 grep,只要你知道要找哪兩個欄位,十四條裡抓得到十三條:漏的那條斷言裡連欄位名都沒有(等一下會看到)。

價值在於你不必先知道要找什麼。


十四條,五種寫法

第二層才去掃測試:逐條 assert,抽出兩邊碰到的欄位,只在兩邊用同一個結果物件時才比對親緣表。跨執行的斷言(res1.x 對 res2.y)不標:那是蛻變關係的正常形狀。

合計標記 14 條,掃了 10 個檔案。

14 / 232 條斷言,6.0%,十份全中。全部落在同一組親緣:圖表餘額對真實餘額,而那兩個欄位在受測的實作上是同一個 tuple(148 行算出來,154 與 156 行指過去)。

同一件事,五種寫法:

res.balances_charted == res.balances_raw                    # 10 條
list(r.balances_charted) == list(r.balances_raw)
len(res.balances_charted) == len(res.balances_raw)
res.balances_charted == pytest.approx(res.balances_raw)
for charted, raw in zip(res.balances_charted, res.balances_raw):
    assert charted == raw

len(x) == len(x) 那條值得停一下:它比的是同一個物件的長度,跟它自己的長度。

親緣表的第二列標記了 0 條。沒有任何一份在同一個結果物件內寫出「缺口等於目標金額減資產」。一把尺如果只會說「有問題」,它沒有用;要看它在該閉嘴的時候閉不閉嘴。


第一版漏了一條,而那道守衛的設計是錯的

上面那五種寫法,第一版的工具只認得四種。

它只會找 變數.欄位 的形狀。最後那種先用 zip 拆成迴圈變數再比較的,它看不見:報表顯示那個檔案「標記 1 條」,而實際上有 2 條。

assert len(res.balances_charted) == len(res.balances_raw)     # 抓到
for charted, raw in zip(res.balances_charted, res.balances_raw):
    assert charted == raw                                     # 沒抓到

順帶一提,這也是 grep 抓不到的那一條:最後那行裡,兩個欄位名一個都沒出現。

漏掉的方式是安靜的。沒有錯誤訊息,沒有警告,就是一個小一點的數字。

修法有兩種,選錯了會很慘。補語法是錯的那種:補完 zip 還有先存進 list 再比、還有生成式、還有各種包一層。補到最後,這支工具就變成它本來要取代的那個 grep——永遠在追語法,而且永遠追不完,還會因為「掃過了、沒命中」給你錯誤的安心。

所以除了補上別名追蹤,還加了一道守衛,讓工具自己承認漏了幾處:把註解與字串剝掉,找出所有同時提到一組親緣欄位的程式行,扣掉已經解釋掉的,剩下的報出來叫人看。

而第一版的守衛,設計是錯的。

它拿「提到欄位的行數」去比「被標記的斷言數」:那是兩組不同的行。run-A-MR-03 的前者是 L261、L262,後者是 L261、L263:只有 L261 重疊,另外兩行各走各的,而兩邊都是 2。

十個檔案全部「相等」,守衛一次都沒響。我差點把這十個「相等」當成「什麼都沒漏」。

它其實沒有放跑任何東西:別名追蹤補上之後,那條 zip 確實被抓到了。問題出在別的地方——那兩個數字數的根本不是同一種東西,所以它們相等或不相等,都不帶任何資訊。 它給我的不是錯誤的結論,是憑空的安心。

一個從不觸發的守衛,跟壞掉的守衛長得一模一樣。

改成比對行號集合之後,再把別名處理關掉模擬第一版:

run-A-MR-03.txt|標記 1 條
  ⚠ 另有 1 行同時提到一組親緣欄位,但工具沒解釋:L262

(這是刻意退回第一版才看得到的輸出。真的跑那十份產物時,守衛一次都沒響。)

這件事寫成第 6 組 KAT:守衛必須被看過響一次,否則它沒有被驗收過。


它看不到的那一半

昨天那條在正確實作上就紅掉的關係:固定年支出與其他收入同時加二十萬,主張目標金額不變,錯在單位(一個是年額,一個程式裡會再乘十二,差了十二倍)。

這支工具看不到它。 那兩個欄位確實來自不同的計算路徑,結構上完全合格。斷言檢核器也看不到,它給了那份 100% 忠實性。

所以分工線畫成三段:

誰做 查得多完整
這支程式有哪些欄位對是親戚 工具(量程式行為) 五個欄位兩兩查過、三個純量的加減也查過。但只查這幾種形式
哪些斷言碰到它們 工具(比對語法) 不完備,所以要自報漏了幾處
這條關係在數學上對不對 人,而且要做完代數 —

第三段目前沒有任何一把尺看得到。


放進工作

工具本身綁死在一個退休金計算機上,抄了沒用。可以抄的是順序——先量程式,再掃測試。

第一步,量你自己的回傳物件。 跑幾百組輸入,欄位兩兩比對,問三件事:

  • 哪兩個永遠是同一個物件?
  • 哪兩個永遠值相等,但不是同一個物件?
  • 哪個永遠等於另外兩個的和或差?

這張表跟測試無關,是程式的性質,做一次可以用很久。而且它會冒出你沒想到的那幾組——我就冒出一組。

第二步,拿那張表去掃測試。 分界線很清楚:同一次呼叫的兩個輸出互比,可疑;跨呼叫的互比,正常。 前者在問「這支程式對自己一致嗎」,後者才是在問「兩次執行之間的關係」。

第三步,讓你的掃描自己承認漏了幾處。 這步最容易省掉,也最不該省。

常見的可疑形狀:

形狀 要查的
resp["count"] == len(resp["list"]) count 是不是回傳前才 len() 出來的
summary.total == sum(rows) total 是不是把 rows 加一次而已
cached == fresh 兩邊是不是同一個查詢

後記

寫這支工具的時候,有一刻覺得自己在做白工。

昨天那個檢查真的只要三十秒。為了一個三十秒的動作寫六組 KAT、一道守衛、兩層報表,怎麼算都不划算。

划不划算的算式錯在一個地方:我拿工具跟「做得對的人工檢查」比。但昨天的事實是,我根本沒查對——我查了一對欄位就以為查完了。它便宜,但它的良率不是一。

比較刺的是後面那段。工具寫完,漏了一條;守衛寫完,是壞的。我一邊寫著「人會略過檢查而且自我感覺良好」,一邊做了一模一樣的事。

差別只有一個:程式碼可以再跑一次,而且跑出來的數字會不一樣。腦子裡的那次檢查不會。


這些數字不能拿來做什麼

  • 6.0% 只對這十份產物、這一個受測物成立。輸出欄位設計不同的程式會得到完全不同的數字
  • 「標記」不等於「沒用」,等於它的兩邊在實作上是親戚,那條斷言約束的東西比它的長相少
  • 親緣表是對這一個實作量的。同一條斷言換一個實作就不是親戚
  • LINEAR 那一列是 200 組輸入上的推論,不是證明。IDENTITY 是 is 判定,那一列才確鑿
  • 親緣表只查「同一個物件」「值恆等」「三個純量的加減」這幾種形式。tuple 對純量、非線性、四個以上欄位的關係,它沒查,也不會告訴你它沒查
  • 第二層是語法比對,它現在也還是不完備,只是會自報
  • 本日沒有事前封存的數字預測。這不是生成實驗,是拿新工具掃已有的產物,跑之前沒預測過會標記幾條,所以不回填一個

工具、親緣表與逐條輸出在 manifest.yaml。


只帶走一件事
要查的不是「這條斷言可疑嗎」,是「這支程式有哪些欄位對是親戚」。
前者要你先看出來,後者是量出來的。


這把尺量的是結構,而且它知道自己漏了什麼。

結構之外那一半,連漏了什麼都不知道。


今天的 repo:https://github.com/eyelash500/2026_ironman_test_ai/tree/day27


上一篇
【Day26】十份 AI 都寫了同一條測試,而它比的是同一個東西
下一篇
【Day28】補上那一格的,是一條 pytest.raises
系列文
AI 寫的測試,誰來測? 共 33 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言