EnriqueMark
設計模式與架構

測試的兩面

2026-09 繁體中文

以測試本身來說,完善覆蓋的深度是沒問題的。但這對於保證一個穩健的程式運作來說是不夠的。因為也許測試很詳細的覆蓋了函式體自身的邏輯、產出的結果,乃至是整體的e2e。但對於輸入本身,卻始終是建立在預設前提之下的。

例如關於輸入資料的假定。此前談到 基於屬性的測試 時我有聊過這方面——關於輸入,這部分是有盲區的。而在這個地方,最適合的就是引入PBT。

經典的測試構建會使用假資料或者是人為(如今是AI)構建的資料來對被測物件進行攻擊。但這裡的顯著問題當然就是「我如何才能測到我想不到」的部分?即便交給AI,它也會受到上下文的影響,仍然會產生忽略的部分。哪怕函式體的單元測試和e2e都已經做的很詳細了,最終仍然出現了不可預料的bug。這個坑我踩到了,復盤之後,我發現之所以導致這個問題的原因在於「維度」。

我發現自己的測試過多集中於「邏輯和輸出」,卻忽略了「輸入」。這就導致了上述所說的問題,儘管邏輯沒問題,在可預期的情況下輸入常規資料進行e2e也沒問題,但在真正執行時卻撞上了「很常見」卻沒被我測到的情況——即便在單一維度打的再深,沒覆蓋到的地方也還是沒覆蓋到。此問題的關鍵即在於「資料」,輸入的資料是一個我在測試時預料之外的情況。這裡的預料之外當然也有我在經驗上的侷限性,但在結構上和「不知道自己不知道」什麼這點是共通的。

因此,我嘗試尋找解決辦法——這就來到了PBT,此前我一直沒正式引入,因為覺得不太有必要而且很繁瑣,要加額外的套件。那能否引入一個輕量級的,即只採取其思路,但先不上重型設施呢?在這樣的思考之下,我開始在輸入側進行補強。但即便如此,如何構建生成器也是一個需要考量的地方,畢竟資料有問題,再怎麼打也測不出實質——一開始我想到從已有資料去構建分佈,但是轉念一想,這麼搞對於新功能就廢了,沒資料的情況下怎麼辦?因此,不能取具體的資料,必須取結構,即在結構之上,其產出可能是什麼樣的?而且要用PBT還有另外一個問題,那就是這裡的值可能不是連續值,而是不同狀態交叉產生的值,怎麼明確到底要測哪些狀態?

到這裡就得引入一個新的測試概念,那就是 組合測試 ,其恰恰就是給這種多輸入引數的組合場景所用的。在組合測試之中,它首先就要求測試者去明確定義每個狀態的意涵,換言之明確當前的狀態邊界,或者說它的空間有多大——這裡的定義不是指型別,是明確的定義其「業務邏輯」,以及在邏輯中該狀態到底幹什麼用的。比如存在狀態A,我需要明定該狀態A的具體意涵是什麼、這個狀態A在該輸入之下應當被如何處置、以及為什麼要按照前述那樣去處置它。光是在這個過程中,許多潛在空間就被消除掉了。

在完成了對多個狀態的定義之後,就要開始圍繞著組合造測試資料了——經典的組合測試會採用結對組合(在n = 2的情況下這就是全組合),就如上述引用的文章所說,這是價效比最高的方法,尤其是防止組合爆炸。在狀態較少的情況下,其實止步於此就夠了,很多狀態組合導致的問題就能測出來了。但是,新問題來了,如果我的狀態產出值是無法列舉的呢?比如時間、金額這種的。這個地方就該PBT出場了,這也是我最輕量引入PBT的地方。

直接用我們前面的組合構建出一個生成器——但新的問題又來了,那就是我很難從具體的數值上斷言到底什麼才是正確的,我無法「預言」結果,而PBT恰恰要求我有一個金標準判斷結果。因此,這個地方我必須採用蛻變測試(Metamorphic testing),即在無法預測輸出結果的情況下,通過斷言關係,從結果是否滿足關係的角度上來判斷輸出是否合法——而這東西,正好就是標準PBT最常用的斷言種類之一。前置準備後,就可以開始大範圍的打了。最終把結果抓出來,並通過 shrinking 收斂到範圍內。

測試構建完成後,自然是進行迴歸。我把這一套新的輸入側補強引入了常規測試,並特別要求了一個無上下文的agent去跑整個測試,看能否在每次開發完都要進行的「例行測試」中復現我此前發現的問題(順帶一提,構建的過程中就出現了他把被測物件預設進去,將測試寫成了迴歸,這是典型的先射箭後畫靶,測不出實際結果,所以必須HITL去給予明確的反饋和監督)。結果是——成功抓到。從事後回測結果來看,這一套確實是有效的。

我覺得這次問題暴露最有效的價值體現在「流程」上。就像我之前在 基於過程的質量控制 中所說的一樣——我個人的AI使用哲學始終是過程至上的。當代製造生產體系已經無數次的表明了流程對錯誤發現和修復的貢獻,因此,相較於一次局部的問題,我會更關心「為什麼我的流程沒有發現它」來作為預警機制。一旦發現問題,除了針對於個別bug構建迴歸之外,對流程的重新檢討也是必要的一環。一次bug修復可以是現在的,但迴歸的價值就在於未來:完善的質量控制流程也是一樣,不只是關乎現在,還同樣關乎到未來。