- Published on
Kiro SDD 入門(二):用 PBT 抓出你測不到的 bug
Table of Contents
上一篇講了 Specs 的三階段,這篇要接一個問題:AI 照著你的 requirements 生了 code,你怎麼知道它真的對?
上一篇:Spec 基礎
先講一般測試的問題
假設你做了一個「儲存 API key」的功能,你會怎麼測?大概就是:
- 存一個 key 叫
"openai",值是"sk-abc123",取出來看對不對 - 再存一個
"anthropic",值是"sk-xyz789",取出來看對不對
測完兩三個案例,看起來都過了,你就覺得沒問題了。
但問題是:你測的都是「你想得到的正常輸入」。如果有人存了一個 provider name 叫 "__proto__" 呢?你不會想到要測這個,AI 寫測試也不會想到。
PBT 的做法:讓電腦幫你想案例
PBT(Property-Based Testing)換了一個思路:你不用自己想案例,你只要告訴框架「什麼規則應該永遠成立」,框架會自動生成大量隨機輸入去測這條規則。
以剛才的 API key 功能來說,規則就是:
不管 provider name 跟 API key 是什麼字串,存進去再取出來,值應該要一樣。
框架會自動生成各種你想不到的字串去測:空字串、超長字串、特殊字元、unicode、還有像 "__proto__" 這種 JavaScript 的保留字。一次跑個 100 組,看有沒有哪一組會讓這條規則不成立。
圖說:一般單元測試只測開發者想得到的正常案例,PBT 則是定義規則後由電腦產生大量隨機輸入,碰撞出未知的邊界漏洞。
真實案例:第 75 次就抓到安全問題
這是 Kiro 官方 blog 記錄的真實案例。
背景
在做一個 chat app,需要存各家 LLM provider 的 API key。Kiro 照著 Spec workflow 走,從 requirements 推出了一條規則(property):「存進去再取出來要一樣」,然後用 fast-check 這個框架去跑 PBT。
程式碼大概長這樣:
fc.assert(
fc.property(fc.string(), fc.string(), async (provider, apiKey) => {
saveApiKey(provider, apiKey);
const retrieved = loadApiKey(provider);
return retrieved === apiKey;
}),
{ numRuns: 100 },
);
fc.string() 會隨機生成字串,numRuns: 100 就是跑 100 組。
跑到第 75 次,fail 了
框架隨機生成了 provider = "__proto__",結果存進去之後取出來的值不對 => fail。
框架接著做了一件很有用的事
fail 之後框架不是直接把那組亂七八糟的隨機輸入丟給你,它會自動幫你「簡化」那個失敗的案例。
這個過程叫 shrinking,概念跟你 debug 的時候一樣:你會試著把問題簡化到最小重現步驟,把不相關的東西一個一個拿掉,直到找出最核心的觸發條件。框架自動幫你做這件事。
簡化完之後,最小的失敗案例是:
- provider:
"__proto__" - apiKey:
" "(一個空白)
圖說:框架透過自動化 Shrinking 過程,一步步過濾掉不相關的干擾與複雜度,最終找出最極簡的核心出錯條件。
apiKey 被簡化到只剩一個空白 => 代表問題跟 value 無關,純粹是 "__proto__" 這個 key 造成的。
為什麼 "__proto__" 會出問題
JavaScript 的物件有一個特殊機制:每個物件都有一個隱藏的 __proto__ 屬性,指向它的原型(prototype)。這是 JavaScript 繼承系統的核心。
所以當你把 "__proto__" 當成一般的 key 去寫入:
obj['__proto__'] = 'some value';
JavaScript 不會把它當成普通的資料存進去,而是嘗試去改這個物件的原型。結果就是:寫入看起來成功了,但讀取的時候拿到的不是你存的值,而是原本的 prototype 物件。
這就是為什麼「存進去再取出來」這條規則被打破了。
修法
用 Object.create(null) 建立一個「沒有原型」的乾淨物件:
const apiKeys = Object.create(null);
一般的 {} 會繼承 Object.prototype,所以 __proto__ 有特殊意義。但 Object.create(null) 建出來的物件沒有原型鏈,__proto__ 就變成一個普通的 key,可以正常存取。
這種 bug 你手動測不會想到,AI 寫測試也不會想到。但 fast-check 的隨機生成器裡面內建了 "__proto__" 這類常見的危險字串(是社群多年累積貢獻的),所以 PBT 會撞到。
PBT 在 Kiro 裡怎麼用
Kiro 不是叫你自己去寫 PBT,它把 PBT 整合進 Specs workflow:
圖說:Kiro 從需求自動推導出驗證規則,並在生成 Tasks 時附上 PBT 測試,幫助開發者提早抓出預期外的 Bug。
- 你用 EARS 寫 requirements(上一篇講的)
- Kiro 在 design 階段自動從 requirements 抽出可以測的規則(properties)
- 生成 tasks 的時候會包含 PBT 測試
- PBT 預設是 optional => 你可以先跑核心實作,確認基本功能沒問題再開 PBT
- 如果 PBT fail,Kiro 會顯示簡化後的最小失敗案例,你可以決定要改 code、改 test、還是改 requirement
PBT 跟 unit test 的關係
這兩個不是二選一,搭配用效果最好:
- unit test:鎖定特定案例,確保已知的 bug 不會再出現
- PBT:用大量隨機輸入去撞你沒想到的邊界
numRuns 預設 100,可以自己調。跑越多信心越高但越花時間,看你的需求。
參考
https://kiro.dev/docs/specs/correctness/https://kiro.dev/blog/property-based-testing-fixed-security-bug/https://kiro.dev/docs/specs/bugfix-specs/
支持作者 ☕
透過 LINE Pay 支持
透過 Ko-fi 支持