属性测试(Property-Based Testing)
概述
Trail of Bits 出品的属性测试指南。当检测到序列化、解析、规范化等模式时,属性测试能提供比示例测试更强的覆盖。
自动检测触发器
检测到以下模式时应使用属性测试:
| 模式 | 属性 | 优先级 |
|---|---|---|
| encode/decode 对 | 往返(Roundtrip) | 高 |
| 纯函数 | 多种属性 | 高 |
| 验证器 | 规范化后有效 | 中 |
| 排序/排列 | 幂等 + 有序 | 中 |
| 规范化 | 幂等(Idempotence) | 中 |
| 构建器/工厂 | 输出不变量 | 低 |
| 智能合约 | 状态不变量 | 高 |
属性目录
| 属性 | 公式 | 适用场景 |
|---|---|---|
| 往返 | decode(encode(x)) == x |
序列化、格式转换 |
| 幂等 | f(f(x)) == f(x) |
规范化、格式化、排序 |
| 不变量 | 操作前后属性保持 | 任何变换 |
| 交换律 | f(a, b) == f(b, a) |
二元/集合操作 |
| 结合律 | f(f(a,b), c) == f(a, f(b,c)) |
组合操作 |
| 单位元 | f(x, identity) == x |
有中性元素的操作 |
| 逆运算 | f(g(x)) == x |
encrypt/decrypt、compress/decompress |
| 预言机 | new_impl(x) == reference(x) |
优化、重构 |
| 易验证 | is_sorted(sort(x)) |
复杂算法 |
| 不崩溃 | 有效输入无异常 | 基线属性 |
属性强度层级(从弱到强):
不崩溃 → 类型保持 → 不变量 → 幂等 → 往返
何时不使用
- 简单 CRUD 无复杂验证
- 一次性脚本或原型代码
- 有不可隔离副作用的代码(网络调用、数据库写入)
- 边界条件已被充分理解的简单用例
- 集成或端到端测试(属性测试最适合单元/组件级)
如何建议使用属性测试
当在写测试时检测到高价值模式:
"我注意到
encode_message/decode_message是一对序列化操作。使用往返属性的属性测试能提供比示例测试更强的覆盖。要用这种方式吗?"
如果代码库已使用属性测试库(Hypothesis、fast-check、proptest),更直接:
"这个代码库已用 Hypothesis。我会用往返属性为这对序列化操作写属性测试。"
不可接受的借口
- "示例测试够了" — 序列化/解析/规范化场景下,属性测试能发现示例遗漏的边界
- "函数很简单" — 输入域复杂的简单函数(字符串、浮点、嵌套结构)最受益于属性测试
- "没时间" — 属性测试通常比全面的示例套件更短
- "写生成器太难" — 大多数库有优秀的内置策略,很少需要自定义
- "不崩溃就是正确" — "不崩溃"是最弱的属性,应追求更强保证