我憑什麼相信綠燈
#QA 思維 #AI #測試策略 #QA 學習
我把自己的程式碼故意改錯,然後跑了測試。 七個測試,全綠。 我改的那一行,是讓文章目錄誤收 h4 標題——一個真實會壞掉的 bug。但我寫的每一個測試都說「沒事」。那一刻卡住我的不是技術,是一個更難的問題:我憑什麼相信「綠燈」? 那個問題,四十年前就有名字 那個名字叫 oracle。 在測試裡,oracle 不是神諭,是你判斷「這次輸出到底對不對」的依據。它可以是一份規格、一個舊版本的答案、一條「不該 crash」的常識,甚至只是你瞇著眼說一句「嗯,看起來合理」。 重點是: 沒有 oracle,你可以把程式跑得飛快,卻沒有任何資格說它是對的。 我那七個綠燈跑得很順——但它們的 oracle 是我自己寫的,而我寫的時候,從沒想過 h4。 這不是 AI 時代才冒出來的新問題。1982 年,Elaine Weyuker 就寫過一篇〈On Testing Non Testable Programs〉,談的正是這件事:很多程式的正確答案,要嘛沒有 oracle,要嘛取得 oracle 的代價高到不切實際。四十年後,AI 一秒生出一千行程式碼、一萬個合成市民、一座 3D 古蹟,我們只是把同一個老問題,放大到了前所未有的規模。 oracle 不是開關,是等級 而 oracle 不是「有」或「沒有」的開關,它有等級。從最可靠到最不可靠,大致是四種: | oracle 種類 | 你的判斷依據 | 可靠度 | | | | | | Specified | 規格明寫「輸入 2 → 輸出 4」 | 最高 | | Derived | 從舊版本、或另一套工具的輸出推回來 | 中 | | Implicit | 沒人寫下來,但「不該 crash、不該是負數」這種常識 | 低 | | Human | 人眼看一下,覺得合理 | 最低、也最貴 | 把 AI 的輸出放進這張表,你會發現它幾乎全擠在底下兩格——沒有規格,只能靠人覺得合理。這就是為什麼在 AI 時代,「生出東西」很便宜,「相信生出來的東西」很貴。便宜的那一半已經自動化了;貴的那一半,還壓在人身上。 我怎麼測我的測試 我那天做的事,有個正式名字: mutation testing (突變測試)。它的邏輯是反過來的——不問「測試會不會通過」,而是故意把程式改錯,看測試殺不殺得掉這個壞掉的版本。每個被植入的錯誤叫一個 mutant ;測試抓到、變紅燈,叫「殺掉」;測試照樣全綠,叫「存活」。一個存活的 mutant,就是你測試網上的一個洞。 這概念也很老——1978 年 DeMillo、Lipton、Sayward 就提出來了。我只是拿它來測我自己 blog 的一個小模組 src/lib/toc.js(把文章標題切成目錄)。它原本有七個測試,全綠。我手動植入三個 mutant: | Mutant | 我改了什麼 | 測試反應 | | | | | | M1 | 標題比對 {2,3} → {2,4}(誤收 h4) | 🟢 七個全綠, 存活 | | M2 | {2,3} → {1,3}(誤收 h1) | 🔴 被殺 | | M3 | 空白轉連字號時不保留長度,一律收成一個 | 🔴 被殺 | 三個裡殺掉兩個,mutation score 2/3。聽起來還行——直到你盯著 M1。 讓目錄誤收 h4,是一個真實會壞掉的行為。可是我七個測試全部給綠燈,因為我從來沒寫過一個餵 h4 的案例。 100% 通過,不代表 100% 守住;它只代表「我想得到的情況」全過了。 我想不到的那一整類,測試完全沉默。 2011 年 Jia 與 Harman 的綜述把這件事講得很白:測試的價值,不在它通過多少,而在它能不能抓出被植入的錯誤。綠燈只證明測試還活著,不證明你安全。 那為什麼我會漏掉 h4? 答案不太舒服:因為測試是我自己寫的。 我寫 toc.js 的時候,腦袋裡的模型就是「文章只有 h2 和 h3」。我帶著這個假設寫程式,然後帶著一模一樣的假設寫測試。h4 不在我的世界裡——所以它既不在程式裡,也不在測試裡。 測試沒抓到盲點,因為它跟程式共用同一個盲點。 我不是在驗證我的程式,我只是把我的假設,用另一種語法又抄了一遍。 測試學科對這件事有個老答案,叫 independent verification :驗證的人,要跟生產的人分開,否則同一套盲點會同時污染兩邊。航太和核能甚至養獨立的 IV&V 團隊,明文規定他們不准跟開發共用假設。Barr 等人 2015 年那篇 oracle problem 的綜述,反覆指向的也是同一個核心——難的從來不是「跑出一個答案」,而是「從哪裡弄到一個不被你自己污染的判斷依據」。 黑箱、白箱、灰箱:你背過,但沒人說真相 每篇測試教學都會講三箱,定義也都差不多,差別只在你照不照得到程式內部。背起來很簡單,落實的時候卻全是落差: | | 教科書說 | 現場真相 | | | | | | 黑箱 | 不看程式,照規格與行為測 | 純黑箱是神話。你一定會偷瞄 log、DB、API 來設計更準的案例 | | 白箱 | 看程式,追覆蓋率 | 100% 行覆蓋 ≠ 測過。我的 toc.js 就是活例:覆蓋率漂亮,h4 照樣漏 | | 灰箱 | 部分知道內部結構 | 這才是所有人實際在做的事,只是沒人替它命名 | Glenford Myers 在《The Art of Software Testing》裡早就說,黑箱是 input/output driven,白箱才看控制流;ISTQB 的詞彙表乾脆把三箱按「對內部的存取程度」排成一條光譜。重點從來不是你站在光譜哪一點,而是—— 你決定讓自己看進去多深。 而 AI 把這個決定的成本整個翻轉了。三箱裡,白箱和灰箱一直比較貴,貴在「讀懂內部」:爬程式、看 schema、追 call stack,都要時間。Claude Code 把這塊砍到趨近於零——貼一個模組進去,它幾秒就讀完內部結構。 於是門檻移位了。當「看進去」變免費,價值就不在「能不能看」,而在三個更難的問題: 該看多深?誰來看?看了之後,會不會像我漏掉 h4 那樣,被自己的假設帶偏? 把這套搬進 Claude Code 回頭看我那個實驗:三個 mutant 是我一行一行手改的。這正是 AI 改變的地方——「機械式地把程式改錯、跑測試、記錄結果」這種工,是 Claude Code 的主場。 自動跑突變。 你可以直接叫它:「列出 toc.js 所有等價變異,逐一植入、跑測試、回報 mutation score。」幾秒鐘,它做完我手動半天的事,還會把存活的 mutant 一條條攤給你看——那些就是你測試網上的洞。要更系統,接 Stryker 這類 JS 突變測試工具,npx stryker run 一次掃整個專案。 讓它讀內部、設計灰箱案例。 把 schema 或 API 定義貼給它,叫它「從使用者介面,設計能打到這個邊界、這個 race condition 的案例」。它同時握著內部結構和外部介面——人做灰箱得左右切換,它是天生的。 但這裡有一條一定要守住的線: ⚠️ 別在「寫程式的那段對話」裡,順手叫它補測試。 那等於讓帶著同一套假設的腦袋,既寫程式又寫測試——你只是把我漏掉 h4 的那個錯,自動化了一遍。要獨立驗證,就開一段新對話、新 session,只丟程式碼進去,不給它你的假設。脈絡乾淨,驗證才算數。 那真的沒有 oracle 的時候呢 你可能會問:講了半天,要是根本沒有正確答案可比對呢?AI 的輸出常常就是這樣——沒有規格、沒有舊版本,只有你瞇眼覺得合理。 測試學科有兩條老路。一是 metamorphic testing (蛻變測試):你不知道正解,但你知道「輸入變了 X,輸出該跟著變 Y」。一段排序,打亂輸入順序,輸出該不變;一句話翻成英文再翻回來,意思該守住。你測的不是值,是關係。Chen 等人 2018 年的綜述,正是把它當成 AI/ML 這種「沒有 oracle」系統的主流解法。 二是把歷史當 oracle。已知結果的舊資料,是天上掉下來的 specified oracle——這就是 backtesting(回測)。我開頭提的那場黑客松裡,有個叫 Sim Francisco 的隊伍(這是別人的專案,不是我那個小實驗)就這樣驗他們的合成市民:拿 2024 年舊金山的選舉回頭算,預測民主黨得票 81.3%,實際 83.8%。差兩個百分點,但說服力全在那個「對得上事實」——他們沒靠「看起來合理」,他們靠了一個誰都改不掉的真相。 價值搬到哪裡去了 寫程式的成本正在崩向零。當「生出一個東西」幾乎免費,瓶頸就整個搬到另一邊:你憑什麼相信它是對的? 這不是 QA 這行的末日,恰恰相反。當每個人都能一秒生出程式、報告、模型,每個人都被迫面對那個我們想了幾十年的問題——oracle 在哪、驗證乾不乾淨、綠燈到底算不算數。差別只在,這套詞彙和直覺,我們練了很久,而他們才剛開始。 所以下次你看到一整排綠燈,先別急著鬆口氣。先問一句: 我憑什麼相信它?