wen aidev
Published on

Kiro SDD 入門(二):用 PBT 抓出你測不到的 bug

上一篇講了 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 組,看有沒有哪一組會讓這條規則不成立。

一般測試 vs. PBT (Property-Based Testing)

圖說:一般單元測試只測開發者想得到的正常案例,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:" "(一個空白)
PBT Shrinking 簡化失敗案例

圖說:框架透過自動化 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:

PBT 在 Kiro Specs Workflow 中的運作

圖說:Kiro 從需求自動推導出驗證規則,並在生成 Tasks 時附上 PBT 測試,幫助開發者提早抓出預期外的 Bug。

  1. 你用 EARS 寫 requirements(上一篇講的)
  2. Kiro 在 design 階段自動從 requirements 抽出可以測的規則(properties)
  3. 生成 tasks 的時候會包含 PBT 測試
  4. PBT 預設是 optional => 你可以先跑核心實作,確認基本功能沒問題再開 PBT
  5. 如果 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 支持

留言討論