判準的推導來源:測試憑什麼不是實作的回音
測試抓出需求被誤讀的能力,取決於它的判準是不是由實作推導出來的。判準另有來源時,測試才有機會撞見實作對需求的誤解;判準從實作推導時,oracle 在這一項上退化成實作自身的回音。其他幾種驗證力不受影響——崩潰、回歸、內部不一致照樣抓得到,下一節的表把界線畫出來。
出處是這條軸最常見的代理,不是這條軸本身。 同一次生成的測試與實作幾乎一定共用推導,所以出處同源通常意味著判準被污染;但兩者不是同一件事,而它們分岔的位置恰好是本模組推薦的做法之一——同一個 agent 同時產出實作與一條往返性質時,出處同源而判準來自需求,驗證力仍在。反過來也成立:兩個獨立會話,而寫測試那一側被餵了既有的 API schema,出處獨立而判準有一部分從實作推導。判定要問的是這條斷言的預期值從哪裡長出來,出處只是最好查的那個線索。這條性質在測試由人手寫的年代很少被單獨討論,因為寫測試的人與寫實作的人即使是同一位,中間也隔著一段時間與一次重新理解需求的動作;產出速度提高之後那段間隔消失了,獨立性變成需要主動維持的性質。
判準被污染的三種形態
判準被實作污染的形態由兩件事決定:寫實作與寫斷言的推理是不是同一段,以及寫斷言時手上有沒有實作可以照抄。兩者組合成四格,而第四格「不同推理、無可抄對象」是後面要談的做法(另一個來源先寫測試)、不是問題形態,所以不在這一節。以下三種按判準被污染的程度由重到輕排列。
同一次產出(同推理、無可抄對象)。 一個請求同時要求實作與測試,兩者由同一段推理產生。獨立性最低,因為需求的每一次誤讀都會被一致地寫進兩邊——這裡沒有可抄的實作,但也不需要抄,誤解在源頭就共用了。
實作完成後補測試(不同推理、有可抄對象)。 實作先存在,之後要求為它補一套測試。測試會照著實作的分支結構長出來,覆蓋率數字通常很高,而斷言的預期值多半是從實作推導的。推理雖然分了兩段,可抄的對象讓第二段推理沒有動力回到需求。
人讀完實作後補測試(不同推理、有可抄對象、但推理者換了)。 人讀過產出再寫測試,獨立性取決於讀的過程有沒有回到需求。讀完一份不熟悉的實作之後,腦中的「它該做什麼」已經被它實際做了什麼佔滿,寫出來的斷言常常是實作行為的複述。這一種比多數人以為的低,而它仍是三者中最高的——換了推理者本身就製造了一部分差異。
三種形態共通的訊號是斷言的寫法:斷言重述實作的計算式(實作寫 total * 0.8、斷言也算 total * 0.8)。預期值只要能從實作推導出來,這條斷言就沒有在驗證任何東西。
判準被污染時還抓得到什麼
同源不等於這組測試沒有價值,它的射程被限縮到特定幾類錯誤:
| 錯誤類別 | 同源的測試抓不抓得到 | 原因 |
|---|---|---|
| 執行期崩潰、空指標、型別不符 | 抓得到 | 測試跑過那條路徑就會炸,與判準來源無關 |
| 後續改動造成的回歸 | 抓得到 | 基準是這次的行為,而基準只要穩定就能偵測變化 |
| 實作內部不一致(兩處算法不同) | 部分抓得到 | 兩處都被覆蓋且結果被比較時才會現形 |
| 需求被誤讀(順序、方向、邊界的語意) | 抓不到 | 誤讀同時存在於實作與斷言,兩邊一致所以全綠 |
| 需求缺漏(沒有人想到的情境) | 抓不到 | 沒有進入實作的情境也不會進入測試 |
最後兩類正是 agent 產出最容易出錯的地方,也是同源測試的系統性盲區。單據合併後明細 id 全部重建(下游持有舊 id 的取消與追加操作從此無聲失效,而畫面看起來一切正常)、危險判定的先後順序、幣別換算的方向、狀態機少一條轉換——這些都是需求語意層的問題,在實作與測試共用同一個誤解時全部通過。
這個盲區不會因為測試變多而縮小。T.C10 裡八份程式的單元測試規模從零到 206 個範例,而 25 條驗收條件對危險判定順序一律沉默——規模成長沒有把任何一份帶進那個維度,因為斷言全部從同一份對需求的理解長出來。
本節的三種形態與四個變數出自機制推導;用來對照的實驗數字只有一份來源,模組入口交代了它支持與不支持哪些宣稱。
把判準移出實作的四個變數
獨立性可以被設計,能動的有四件事。前三件是本節展開的重點,第四件在開場已經出現過——時間間隔:寫實作與寫測試之間隔一段時間,中間發生一次重新理解需求的動作。它在人手寫的年代是免費附贈的,產出速度提高之後要刻意安排,而它也是四者裡最不可靠的一個(間隔不保證有人真的回去讀需求)。
需求的來源。 把驗收條件寫成一份人審過的文件,測試的預期值從那份文件逐條取得。這是四者裡效果最強的一個,因為它直接把 oracle 的來源移到實作之外。這份文件自己會漂移,而漂移的方向剛好抵銷它的作用——實作改了之後有人回來把它「對齊現況」,判準就在維護環節被實作污染了。所以它要跟程式碼同 repo、同一個 PR 送審,而且變更要由不是實作者的人核可;做不到這一點時,依同步期待要嘛有機制承接、要嘛明示降級把它降級成標記消費時點的記錄,不要繼續當它是最新的。成本落在那份文件必須寫到可以逐條對應成斷言的程度——寫成「訂單要正確處理」沒有用,要寫到「同一個房間裡同時有蝙蝠與陷阱時,先觸發哪一個」這種顆粒度。條件顆粒度的設計在驗收條件的等價類那章展開。
產出的順序。 測試先寫、實作後補,讓實作沒有可照抄的對象。這正是 Test-First 在 agent 產出情境下的新理由——它原本的理由是「小步好想、介面問題早暴露」,服務的是人的認知負荷;現在它多了一個結構性的理由,就是製造出處差異。順序帶來的獨立性有上限:先寫的測試如果也由同一個 agent 從同一段需求描述生成,誤讀仍然會一致地進入兩邊,順序只擋住了「照著實作寫斷言」這一種退化。
產出的主體。 測試與實作由不同的 agent、不同的會話、或不同的人產出,且寫測試的一方看不到實作。判準是寫測試那一側看不到實作;「開兩個獨立的會話、只餵需求不餵程式碼」是寫作當下達成它的方式,而不是判準本身——跨會話記憶或共用專案 context 會讓兩個會話不再等於隔離,用之前先確認手上的工具是哪一種,成本是需求描述必須自足——寫測試那一側沒有實作可以參照,需求裡漏掉的部分就會直接變成測試的缺口。這個成本本身是有用的訊號,但它有一個判不開的地方:寫不出測試也可能是那一方缺領域上下文或經驗不足,同一個觀察對應得到兩個原因。要分開只有一種做法——同一份需求給兩個獨立的測試方,兩方卡在同一處才是需求的問題。
主體這條線往上還有兩層,而本模組其餘部分預設它們不存在。 「產出的主體」講的是 agent 與會話,那是最裡面那一層;外面還有組織層與契約層:
| 層 | 這一層的主體是誰 | 它決定什麼 | 不在同一方時會怎樣 |
|---|---|---|---|
| 產出 | 哪個 agent、哪次會話 | 判準與實作共不共用推導 | 本節四個變數處理的就是這一層 |
| 組織 | 寫需求的人、寫實作的人、驗收的人 | 誰有能力寫出可對應成斷言的條件 | 外包時驗收測試由乙方寫是同源的組織版;改由甲方寫則甲方多半沒有那個能力 |
| 契約與監理 | 客戶、主管機關、內稽 | 哪些閘門是外部義務、不由團隊決定 | 覆蓋率門檻寫進合約、或 code review 是變更管理控制項時,本模組的修法會與義務衝突 |
三層的判準相同(判準是不是由實作推導),可動的手段不同。本模組後面三章的修法都寫在產出層——決定權、滿足義務、後果承擔屬於同一方是它們共同的前提。這個前提不成立時,先確認是哪一層的主體不同,再看那一層有沒有可動的變數;契約層通常沒有可動的變數,此時要動的是合約或控制程序本身,那超出本模組的射程——但不代表沒有事情可做:外部門檻照舊守著、內部指標另外疊加上去,那條原則在品質閘門的更替的導入順序段。
四者可以疊加,也可以只動一件——但只動一件時要知道拿回了多少:動「需求的來源」把判準整個移出實作,語意層的盲區關得起來;只動順序或時間間隔只擋掉「照著實作寫斷言」這一種退化,共用誤讀那一種仍在,此時定位仍然是回歸網。都動不了的情況下,這組測試的定位要誠實降級成回歸網——它守得住「後來改壞了」,守不住「一開始就寫錯了」——並且在別處補上獨立的判準。
人審的位置要跟著移動
逐行讀 agent 寫的實作,讀的人已經被實作的敘事引導:變數命名、註解、結構都在說服讀者這段程式做的就是它該做的事,而讀者手上沒有一份獨立的對照。這種閱讀能發現的多半是風格與局部缺陷,發現不了「整段做的是另一件事」。
同樣的注意力放在驗收條件上,審的是需求本身,而在需求、驗收條件、實作、測試這條鏈上,驗收條件是少數還沒有被實作污染的環節之一。這也是判斷「不再逐行讀產出」這個決定是否成立的方式:說得出哪一個環節接手了原本由閱讀承擔的那件事,這個決定就成立;說不出來時,交換掉的是驗證的可見性而不是閱讀的時間。
邊界:判準取自實作也合理的場合
一次性的探索腳本、用完就丟的資料轉換(「用完就丟」是對未來的預測,要說得出丟棄的時點或條件,說不出來的多半不會被丟)、以及characterization test 這種本來就以現狀為判準的測試,同源不構成問題——前兩者的錯誤代價低到不值得建立獨立判準,後者的設計意圖就是複製現狀。
重構是另一種邊界,而且它的正確配置不在本節四個變數裡:重構的 oracle 是改動前的行為而不是需求文件,所以「把驗收條件寫成人審過的文件」在這裡等於重建二十年的規格。可行的配置是拿舊實作當參照 oracle(新舊並行跑、比對輸出),配上行為快照守住尚未被正確性測試覆蓋的部分。
另一個邊界是需求本身沒有語意深度的功能:CRUD 端點的欄位對映、單純的格式轉換,誤讀的空間小,同源測試的盲區也隨之縮小。判斷方式是問這個功能有沒有「兩件事同時發生時該怎麼辦」這類問題,沒有的話同源的風險確實低。
判讀訊號
| 訊號 | 該做的事 |
|---|---|
| 斷言的預期值是實作算式的複述 | 回到需求文件取預期值;取不到就是需求沒寫到那個顆粒度 |
| 一次請求同時產出實作與測試 | 拆成兩次,並讓寫測試的那次看不到實作 |
| 覆蓋率數字很高而線上故障集中在語意層(順序、方向) | 這是同源的典型指紋,補測試沒有用,要補的是獨立的驗收條件 |
| 「我讀過了,測試也是照著需求寫的」 | 問寫測試的當下手上有沒有實作;有的話獨立性比想像中低 |
| 需求描述短到另一個人無法據以寫出測試 | 那份描述也不足以讓實作正確,先補需求再談測試 |
| 決定不再逐行讀產出 | 說出哪一個環節接手了原本由閱讀承擔的那件事,說不出來就先不要做這個決定 |
下一步路由
#testing #test-provenance #ai-generated-code #test-oracle #tdd