你寫 add(2, 3) 回傳 5,這個測試過了。但你有沒有測過 add(-2147483648, -1)?或者 add(0.1, 0.2)?或者 add(NaN, 3)

Example-based testing 只測你想到的例子。你的測試覆蓋率很高,但你的想像力是有限的——你想不到的邊界,就測不到。

Property-based testing 把這個問題倒過來:你描述「對所有合法輸入,某個性質應該成立」,框架生成大量隨機輸入來嘗試推翻這個性質。


什麼是 Property

Property 是對函式行為的數學性質描述,不是「某個輸入應該得到某個輸出」,而是「任何輸入都應該滿足某個關係」。

// Example-based test(你想到的 case)
test('reverse', () => {
  expect(reverse([1, 2, 3])).toEqual([3, 2, 1])
})
 
// Property-based test(對所有 array 都成立的性質)
// property: 反轉兩次 = 原始陣列
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    expect(reverse(reverse(arr))).toEqual(arr)
  })
)

第二個測試會自動生成幾百個不同的 array——空陣列、單元素、負數、重複值、超大陣列——來驗證「反轉兩次等於原始」這個性質。你不需要手寫每一個 case。


常見的 Property 類型

Round-trip property:序列化後反序列化 = 原始值。JSON.parse(JSON.stringify(x)) === x 對合法值應該成立。

Idempotency:操作兩次 = 操作一次。deduplicate(deduplicate(arr)) 應該等於 deduplicate(arr)

Symmetrysort(reverse(arr)) 應該等於 sort(arr)

Invariant:操作前後某個性質不變。排序後陣列長度不變、排序後每個元素仍在原始集合裡。

這些 property 的好處是:不需要計算「正確答案是什麼」,只需要描述「關係應該成立」——這讓測試可以覆蓋無窮多的輸入,而不是你手寫的幾個。


工具

JavaScript / TypeScript — fast-checkfc.integer()fc.string()fc.array() 等 arbitrary,加上 fc.property()fc.assert()。找到反例時會自動 shrink 到最小化的失敗 case。

Python — Hypothesis@given(st.integers(), st.text()) 裝飾器。和 pytest 整合,找到問題會自動縮小並記錄在 .hypothesis/ 資料夾供下次重現。

Java — jqwik:JUnit 5 extension,@Property + @ForAll 標記。


和 Example-based Test 的分工

Property-based testing 不取代 example test,兩者分工:

  • Example test:你知道這個具體的 case 很重要,就直接寫。add(2, 3) === 5 是你對業務的明確說明,清楚易讀。
  • Property test:你想確認某個性質對所有輸入都成立,讓機器去找 edge case。

最有效的用法是在關鍵算法(解析器、序列化、排序、加密)和業務規則(優惠計算、退款邏輯)上補一層 property test,讓機器去壓測你沒想到的輸入組合。