Skip to Content

31d - AI辅助PBT

本文是《AI Agent 实战手册》第 31 章第 4 节。 上一节:31c-需求到测试用例自动化 | 下一节:31e-测试Steering规则与反模式

概述

Property-Based Testing(PBT,基于属性的测试)是 AI 辅助测试中最具杠杆效应的技术之一。与传统单元测试逐个验证具体示例不同,PBT 通过定义”对所有有效输入都应成立的属性”,让框架自动生成成百上千个随机输入来验证代码正确性。在 AI Agent 辅助开发的 2025-2026 年,PBT 的价值更加凸显:AI 可以从 Spec 文档中自动发现属性、设计智能生成器、优化收缩策略,将原本需要深厚测试经验的 PBT 编写过程大幅简化。本节深入覆盖 AI 辅助 PBT 的完整工作流——从属性发现、生成器设计、框架选型到收缩优化和反例分析。

💡 本节与第 18 章 18d-PBT深度指南 互补:18d 侧重 PBT 框架本身的深度讲解,本节侧重 AI Agent 如何辅助 PBT 的全流程——从 Spec 发现属性、AI 生成生成器、到 AI 分析反例。


1. PBT 核心概念回顾

1.1 为什么 AI 时代更需要 PBT

AI 生成的代码面临三个独特挑战,PBT 恰好能应对:

挑战传统测试的局限PBT 的优势
AI 在边缘场景犯错人工难以穷举所有边缘输入自动生成包含边界值、空值、极端值的输入
AI 代码的”看起来对”陷阱示例测试可能恰好避开了 bug随机输入能探索到人类未预见的失败路径
需求理解偏差测试和代码可能犯同样的理解错误属性定义迫使你从”应该满足什么”角度思考
重构后的回归风险AI 重构可能引入微妙的行为变化属性测试不依赖具体实现,重构后仍然有效

1.2 PBT 的核心工作流

┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 定义属性 │ │ 设计生成器 │ │ 运行测试 │ │ (Property) │────▶│ (Generator) │────▶│ (Execute) │ └──────────────┘ └──────────────┘ └──────────────┘ ▲ │ │ 通过? │ 失败? │ ┌──────┴──────┐ │ ▼ ▼ │ 增加样本数 ┌──────────┐ │ 继续测试 │ 收缩 │ │ │ (Shrink) │ │ └────┬─────┘ │ ▼ │ 最小反例 │ (Minimal Counterexample) │ │ └───────────── AI 分析反例 ◄────────────────────┘ 调整属性/修复代码

1.3 七种常见属性模式

模式描述典型示例AI 发现难度
往返(Roundtrip)encode 后 decode 得到原值decode(encode(x)) == x⭐ 低
幂等(Idempotent)操作多次与一次结果相同sort(sort(xs)) == sort(xs)⭐ 低
不变量(Invariant)操作后某条件始终成立len(filter(xs)) <= len(xs)⭐⭐ 中
等价/Oracle两种实现产生相同结果fastSort(xs) == referenceSort(xs)⭐⭐ 中
变形(Metamorphic)输入变换后输出满足已知关系sort(reverse(xs)) == sort(xs)⭐⭐⭐ 高
归纳(Inductive)小问题的解可组合为大问题的解sum(xs ++ ys) == sum(xs) + sum(ys)⭐⭐⭐ 高
统计(Statistical)大量样本的统计特征满足预期随机数生成器的均匀分布⭐⭐⭐⭐ 极高

2. AI 辅助属性发现

2.1 从 Spec 文档发现属性

在 Spec 驱动的开发工作流中(如 Kiro 的 requirements.md → design.md → tasks.md),AI 可以系统性地从需求文档中提取可测试的属性。这是 AI 辅助 PBT 最核心的能力。

属性发现的四层模型

┌─────────────────────────────────────────────────┐ │ 第 1 层:需求文档 (requirements.md) │ │ "用户注册时,邮箱必须唯一" │ │ → 属性:对任意两个成功注册的用户,email 不相同 │ ├─────────────────────────────────────────────────┤ │ 第 2 层:设计文档 (design.md) │ │ "排序算法使用归并排序,时间复杂度 O(n log n)" │ │ → 属性:输出有序 + 长度不变 + 元素集合不变 │ ├─────────────────────────────────────────────────┤ │ 第 3 层:API 契约 (OpenAPI/GraphQL Schema) │ │ "GET /users/{id} 返回 200 或 404" │ │ → 属性:对任意有效 id,状态码 ∈ {200, 404} │ ├─────────────────────────────────────────────────┤ │ 第 4 层:代码实现 (源代码) │ │ 函数签名 + 类型约束 + 注释 │ │ → 属性:类型不变量、前置/后置条件 │ └─────────────────────────────────────────────────┘

从验收标准到属性的转换规则

验收标准模式属性类型示例
”WHEN X, THEN Y”条件不变量∀ input: if precondition(input) then postcondition(f(input))
”必须唯一”唯一性属性∀ a, b: f(a) == f(b) → a == b
”不超过 N”边界属性∀ input: result <= N
”格式为 XXX”格式属性∀ input: matches(f(input), pattern)
”A 和 B 结果相同”等价属性∀ input: f_a(input) == f_b(input)
”操作可撤销”往返属性∀ input: undo(do(input)) == input

2.2 AI 属性发现的 Prompt 模板

模板 1:从需求文档发现属性

你是一位 Property-Based Testing 专家。请分析以下需求文档, 为每个验收标准识别可测试的属性。 ## 需求文档 [粘贴 requirements.md 内容] ## 输出格式 对每个验收标准,请提供: 1. 验收标准编号和描述 2. 是否可作为 PBT 属性测试(是/否/部分) 3. 如果可以,写出属性的形式化描述 4. 建议的生成器策略(输入空间如何约束) 5. 预期的边缘场景 请用以下格式输出: ### AC [编号]: [描述] - **可测试性**: [是/否/部分] - **属性**: ∀ [变量]: [属性描述] - **生成器**: [生成器策略] - **边缘场景**: [列表]

模板 2:从代码签名发现属性

分析以下函数签名和文档注释,识别所有可测试的属性。 重点关注: 1. 往返属性(如果存在逆操作) 2. 幂等属性(重复调用是否等价) 3. 不变量(输出必须满足的条件) 4. 单调性(输入增大时输出的变化方向) 5. 分配律/结合律等代数属性 ## 代码 [粘贴函数签名和注释] 对每个发现的属性,请提供: - 属性名称和形式化描述 - 对应的 [框架名] 测试代码 - 推荐的生成器约束

模板 3:从 Kiro Design 文档发现正确性属性

你是 Kiro Spec 驱动开发的 PBT 专家。请分析以下 design.md 中的 Correctness Properties 部分,将每个属性转化为可执行的 Property-Based Test。 ## Design 文档 [粘贴 design.md 的 Correctness Properties 部分] ## 目标框架 [fast-check / Hypothesis / proptest / Go rapid] ## 输出要求 对每个属性: 1. 属性的自然语言描述 2. 形式化表达 3. 完整的测试代码(包含生成器定义) 4. 预期的收缩行为 5. 与 requirements.md 的可追溯性链接

2.3 属性发现的系统化方法

AI 在发现属性时,应遵循以下系统化检查清单:

属性发现检查清单: □ 往返属性 - 是否存在 encode/decode、serialize/deserialize、compress/decompress 对? - 是否存在 create/delete、push/pop、enqueue/dequeue 对? □ 幂等属性 - 排序、去重、格式化、规范化操作是否幂等? - HTTP PUT/DELETE 是否幂等? □ 不变量 - 集合操作后大小关系是否成立?(filter 后 ≤ 原长度) - 数值操作后范围是否成立?(百分比 ∈ [0, 100]) - 类型约束是否始终满足?(非空、非负、有效枚举值) □ 等价/Oracle 属性 - 是否存在简单但慢的参考实现? - 是否存在已知正确的第三方库可对比? □ 变形属性 - 输入排列是否影响输出?(排序不受输入顺序影响) - 输入缩放是否有已知的输出关系? □ 保持属性 - 操作是否保持元素集合不变?(排序不增删元素) - 操作是否保持某种度量不变?(旋转保持距离) □ 交换律/结合律 - 二元操作是否满足交换律? f(a, b) == f(b, a) - 二元操作是否满足结合律? f(f(a, b), c) == f(a, f(b, c))

工具推荐

工具用途价格适用场景
Kiro Specs从 requirements/design 文档提取属性免费(Kiro 内置)Spec 驱动的 PBT 工作流
Claude Code分析代码签名,生成属性和测试$20/月(Pro)复杂业务逻辑的属性发现
Cursor内联属性建议和测试生成$20/月(Pro)编码时实时属性发现
GitHub Copilot基于上下文的属性补全$10/月(Individual)简单属性的快速补全
ChatGPT + Canvas交互式属性探索和可视化$20/月(Plus)学习和探索属性模式

3. 生成器设计

3.1 生成器设计原则

生成器(Generator/Arbitrary/Strategy)是 PBT 的核心——它决定了测试能探索多大的输入空间。好的生成器应该:

  1. 覆盖边界值:空集合、零值、最大值、最小值
  2. 约束到有效输入空间:不生成无意义的输入(如负数年龄)
  3. 偏向”有趣”的值:更频繁地生成容易触发 bug 的值
  4. 支持高效收缩:失败时能快速缩小到最小反例

3.2 各框架的生成器 API 对比

能力fast-check (JS/TS)Hypothesis (Python)proptest (Rust)rapid (Go)QuickCheck (Haskell)
基础类型生成fc.integer()st.integers()any::<i32>()rapid.Int()arbitrary
范围约束fc.integer({min, max})st.integers(min, max)0..100i32rapid.IntRange(0, 100)choose(0, 100)
字符串生成fc.string()st.text()"[a-z]+" (正则)rapid.String()arbitrary
集合生成fc.array()st.lists()prop::collection::vec()rapid.SliceOf()listOf
组合生成器fc.record()st.fixed_dictionaries()(arb1, arb2)结构体字段liftA2
条件过滤.filter().filter()prop_filter!.Filter()suchThat
映射变换.map().map()prop_map!.Map()fmap
扁平映射.chain().flatmap()prop_flat_map!>>=
自定义收缩内置自动收缩内置自动收缩内置自动收缩内置自动收缩需手动实现

3.3 智能生成器设计模式

模式 1:分层生成(Layered Generation)

先生成结构骨架,再填充细节。适用于复杂嵌套数据结构。

fast-check 示例

import fc from 'fast-check'; // 生成一个有效的电商订单 const orderArbitrary = fc.record({ id: fc.uuid(), customer: fc.record({ name: fc.string({ minLength: 1, maxLength: 100 }), email: fc.emailAddress(), }), // 先生成 1-10 个商品项 items: fc.array( fc.record({ productId: fc.uuid(), name: fc.string({ minLength: 1 }), price: fc.integer({ min: 1, max: 1_000_000 }), // 分为单位 quantity: fc.integer({ min: 1, max: 99 }), }), { minLength: 1, maxLength: 10 } ), // 折扣率 0-50% discountPercent: fc.integer({ min: 0, max: 50 }), }).map(order => ({ ...order, // 计算总价(派生字段) total: order.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) * (100 - order.discountPercent) / 100, })); // 属性:订单总价始终非负 fc.assert( fc.property(orderArbitrary, (order) => { return order.total >= 0; }) );

模式 2:状态机生成(Stateful Generation)

生成一系列操作序列,验证状态转换的正确性。

Hypothesis 示例

from hypothesis import given, settings from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, rule, invariant class ShoppingCartMachine(RuleBasedStateMachine): """购物车状态机测试""" def __init__(self): super().__init__() self.cart = ShoppingCart() self.expected_items: dict[str, int] = {} @rule( product_id=st.text(min_size=1, max_size=20), quantity=st.integers(min_value=1, max_value=10) ) def add_item(self, product_id: str, quantity: int): """添加商品到购物车""" self.cart.add(product_id, quantity) self.expected_items[product_id] = ( self.expected_items.get(product_id, 0) + quantity ) @rule(product_id=st.text(min_size=1, max_size=20)) def remove_item(self, product_id: str): """从购物车移除商品""" self.cart.remove(product_id) self.expected_items.pop(product_id, None) @invariant() def items_match(self): """不变量:购物车内容与预期一致""" for pid, qty in self.expected_items.items(): assert self.cart.get_quantity(pid) == qty @invariant() def total_non_negative(self): """不变量:总价始终非负""" assert self.cart.total() >= 0 # 运行状态机测试 TestShoppingCart = ShoppingCartMachine.TestCase

模式 3:约束求解生成(Constraint-Based Generation)

当输入之间存在复杂约束关系时,使用 flatmap/chain 确保生成的数据满足约束。

fast-check 示例

// 生成一个有效的日期范围(start <= end) const dateRangeArbitrary = fc .tuple( fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }), fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }) ) .map(([a, b]) => a <= b ? { start: a, end: b } : { start: b, end: a }); // 生成一个有效的分页请求(offset + limit <= total) const paginationArbitrary = fc .integer({ min: 1, max: 10000 }) // total .chain(total => fc.tuple( fc.constant(total), fc.integer({ min: 0, max: total }), // offset fc.integer({ min: 1, max: 100 }) // limit ) ) .map(([total, offset, limit]) => ({ total, offset, limit }));

模式 4:领域特定生成器(Domain-Specific Generators)

为特定业务领域构建可复用的生成器库。

Go rapid 示例

package generators import ( "pgregory.net/rapid" "testing" ) // 生成有效的中国手机号 func PhoneNumber() *rapid.Generator[string] { prefixes := []string{"130", "131", "132", "133", "134", "135", "136", "137", "138", "139", "150", "151", "152", "153", "155", "156", "157", "158", "159", "186", "187", "188", "189"} return rapid.Custom(func(t *rapid.T) string { prefix := rapid.SampledFrom(prefixes).Draw(t, "prefix") suffix := rapid.StringMatching(`[0-9]{8}`).Draw(t, "suffix") return prefix + suffix }) } // 生成有效的 IPv4 地址 func IPv4Address() *rapid.Generator[string] { return rapid.Custom(func(t *rapid.T) string { octets := make([]int, 4) for i := range octets { octets[i] = rapid.IntRange(0, 255).Draw(t, "octet") } return fmt.Sprintf("%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]) }) } // 生成有效的金额(分为单位,避免浮点精度问题) func MoneyInCents(min, max int64) *rapid.Generator[int64] { return rapid.Int64Range(min, max) } // 属性测试:手机号格式验证 func TestPhoneNumberFormat(t *testing.T) { rapid.Check(t, func(t *rapid.T) { phone := PhoneNumber().Draw(t, "phone") if len(phone) != 11 { t.Fatalf("手机号长度应为 11,实际为 %d: %s", len(phone), phone) } }) }

3.4 AI 辅助生成器设计的 Prompt 模板

你是一位 PBT 生成器设计专家。请为以下数据类型设计智能生成器。 ## 数据类型定义 [粘贴类型定义 / 接口 / struct] ## 业务约束 [列出字段间的约束关系,如 start_date < end_date] ## 目标框架 [fast-check / Hypothesis / proptest / rapid] ## 要求 1. 生成器必须只产生满足所有业务约束的有效数据 2. 偏向生成边界值(空字符串、零值、最大值) 3. 支持高效收缩(优先使用 map/chain 而非 filter) 4. 提供生成器的单元测试,验证生成的数据确实满足约束 5. 如果存在可复用的子生成器,请提取为独立函数

操作步骤

步骤 1:分析数据类型和约束

让 AI 分析目标数据类型的所有字段、类型约束和业务规则:

请分析以下 TypeScript 接口,列出: 1. 每个字段的类型约束(范围、格式、可选性) 2. 字段间的依赖关系(如 endDate > startDate) 3. 隐含的业务规则(如 discount 不能超过 total) 4. 边界值列表(每个字段的极端有效值) interface Order { id: string; items: OrderItem[]; discount: number; total: number; status: 'pending' | 'paid' | 'shipped' | 'delivered'; createdAt: Date; updatedAt: Date; }

步骤 2:选择生成策略

根据约束复杂度选择合适的生成策略:

约束类型推荐策略示例
无约束直接使用内置生成器fc.string()
范围约束参数化内置生成器fc.integer({min: 0, max: 100})
格式约束正则或自定义生成器fc.stringMatching(/^[A-Z]{2}\d{6}$/)
字段间依赖chain/flatmap先生成 start,再基于 start 生成 end
复杂业务规则自定义 Arbitrary + map生成原始数据后 map 到有效状态
状态依赖状态机生成器Hypothesis StatefulTesting

步骤 3:实现并验证生成器

// 验证生成器本身的正确性 fc.assert( fc.property(orderArbitrary, (order) => { // 生成器应该只产生有效订单 return ( order.items.length >= 1 && order.total >= 0 && order.createdAt <= order.updatedAt ); }), { numRuns: 10000 } // 大量运行验证生成器质量 );

4. PBT 框架对比与选型

工具推荐

框架语言用途价格适用场景
fast-check JS/TS前端和 Node.js 的 PBT免费(MIT)React/Vue/Node.js 项目
Hypothesis PythonPython 生态最成熟的 PBT免费(MPL 2.0)Django/FastAPI/数据处理
proptest RustRust 生态主流 PBT免费(MIT/Apache)Rust 系统编程
rapid GoGo 现代 PBT 库免费(MPL 2.0)Go 微服务/CLI 工具
QuickCheck HaskellPBT 鼻祖免费(BSD-3)Haskell/函数式编程
jqwik Java/KotlinJVM 生态 PBT免费(EPL 2.0)Spring Boot/企业级 Java
testing/quick GoGo 标准库 PBT免费(Go 标准库)简单属性测试(功能有限)
pbt RustRust 新一代 PBT(2025)免费(MIT)需要更好收缩的 Rust 项目

框架选型决策树

你的项目使用什么语言? ├── JavaScript/TypeScript │ └── → fast-check(唯一成熟选择,生态完善) ├── Python │ └── → Hypothesis(事实标准,支持状态机测试) ├── Rust │ ├── 需要 Hypothesis 风格的策略组合? │ │ └── → proptest(最成熟,75M+ 下载) │ └── 需要更好的收缩? │ └── → pbt(2025 新库,收缩优于 proptest) ├── Go │ ├── 简单属性测试? │ │ └── → testing/quick(标准库,零依赖) │ └── 需要完整 PBT 功能? │ └── → rapid(现代设计,自动收缩) ├── Java/Kotlin │ └── → jqwik(JUnit 5 集成,功能完整) └── Haskell └── → QuickCheck(PBT 鼻祖,生态最丰富)

各框架的 AI 友好度评估

维度fast-checkHypothesisproptestrapidQuickCheck
AI 生成代码质量⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
LLM 训练数据量丰富非常丰富中等较少丰富
API 直觉性高(链式调用)高(装饰器)中(宏语法)高(函数式)低(类型类)
错误信息可读性优秀优秀良好良好一般
文档完整度优秀优秀良好一般优秀

💡 AI 友好度指 LLM 生成该框架测试代码的准确率。fast-check 和 Hypothesis 因为训练数据丰富、API 设计直觉,AI 生成的代码质量最高。


5. 从 Kiro Spec 发现属性的完整工作流

5.1 Kiro Spec 驱动的 PBT 工作流

Kiro 的 Spec 驱动开发流程天然适合 PBT:

requirements.md design.md tasks.md ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ 验收标准 │ │ Correctness │ │ PBT 任务 │ │ (AC) │───▶│ Properties │───▶│ (带属性编号) │ │ │ │ (正确性属性) │ │ │ │ WHEN X │ │ P1: ∀x: f(x)≥0 │ │ 实现 P1 测试 │ │ THEN Y │ │ P2: roundtrip │ │ 实现 P2 测试 │ └──────────────┘ └──────────────────┘ └──────────────┘

5.2 实战示例:从 Spec 到 PBT

假设我们有一个文件同步工具的 Spec:

requirements.md(节选)

### Requirement 3: 文件差异计算 #### Acceptance Criteria 3.1 WHEN 两个相同的文件被比较, THEN 差异结果为空 3.2 WHEN 对文件 A 应用差异补丁, THEN 结果等于文件 B 3.3 WHEN 差异计算完成, THEN 补丁大小不超过两个文件大小之和

design.md(节选)

## Correctness Properties P1: Roundtrip — 对任意文件对 (a, b),apply(a, diff(a, b)) == b P2: Empty diff — 对任意文件 a,diff(a, a) == empty P3: Patch size bound — 对任意文件对 (a, b), size(diff(a, b)) <= size(a) + size(b)

AI 生成的 PBT 代码(fast-check)

import fc from 'fast-check'; import { diff, apply, DiffResult } from '../src/diff'; // 文件内容生成器:模拟真实文件的字节序列 const fileContentArb = fc.oneof( fc.uint8Array({ minLength: 0, maxLength: 1000 }), // 小文件 fc.uint8Array({ minLength: 1000, maxLength: 10000 }), // 中等文件 fc.constant(new Uint8Array(0)), // 空文件(边界值) ); describe('文件差异计算 - Property-Based Tests', () => { /** * P1: Roundtrip 属性 * Validates: Requirements 3.2 * 对任意文件对 (a, b),apply(a, diff(a, b)) == b */ test('P1: apply(a, diff(a, b)) should equal b', () => { fc.assert( fc.property( fileContentArb, fileContentArb, (fileA, fileB) => { const patch = diff(fileA, fileB); const result = apply(fileA, patch); expect(result).toEqual(fileB); } ), { numRuns: 500 } ); }); /** * P2: Empty diff 属性 * Validates: Requirements 3.1 * 对任意文件 a,diff(a, a) 应为空 */ test('P2: diff(a, a) should be empty', () => { fc.assert( fc.property(fileContentArb, (file) => { const result = diff(file, file); expect(result.changes).toHaveLength(0); }), { numRuns: 500 } ); }); /** * P3: Patch size bound 属性 * Validates: Requirements 3.3 * 补丁大小不超过两个文件大小之和 */ test('P3: patch size should not exceed sum of file sizes', () => { fc.assert( fc.property( fileContentArb, fileContentArb, (fileA, fileB) => { const patch = diff(fileA, fileB); const patchSize = JSON.stringify(patch).length; const maxSize = fileA.length + fileB.length; return patchSize <= maxSize; } ), { numRuns: 500 } ); }); });

5.3 需求可追溯性

每个 PBT 测试都应通过注释链接回需求文档:

requirements.md AC 3.2 design.md Property P1 tasks.md "实现 P1 测试" diff.property.test.ts test('P1: roundtrip') // Validates: Requirements 3.2

这种可追溯性确保:

  • 每个需求都有对应的属性测试
  • 需求变更时能快速定位需要更新的测试
  • 代码审查时能验证测试覆盖了所有需求

6. 收缩优化(Shrinking)

6.1 收缩的工作原理

当 PBT 发现一个失败的输入时,收缩器(Shrinker)会尝试将其简化为最小的仍然失败的输入。这是 PBT 最强大的特性之一——它把”在输入 [847, -23, 0, 512, -1, 99, 0, 3, -456, 72] 时失败”简化为”在输入 [-1, 0] 时失败”。

发现失败输入: [847, -23, 0, 512, -1, 99, 0, 3, -456, 72] 收缩过程 第 1 轮: [847, -23, 0, 512, -1] ← 尝试取前半部分 仍然失败 ✓ 继续收缩 第 2 轮: [-23, 0, -1] ← 移除不影响失败的元素 仍然失败 ✓ 继续收缩 第 3 轮: [-1, 0] ← 进一步简化 仍然失败 ✓ 继续收缩 第 4 轮: [-1] ← 尝试单元素 通过了 ✗ 回退到上一步 第 5 轮: [0] ← 尝试另一个单元素 通过了 ✗ 回退 最小反例: [-1, 0] ← 无法进一步收缩

6.2 两种收缩策略

策略代表框架原理优点缺点
类型级收缩QuickCheck (Haskell)为每个类型定义 shrink 函数收缩速度快需要手动实现,组合困难
集成收缩Hypothesis, fast-check, proptest收缩与生成器绑定,自动推导零配置,组合自然收缩路径可能不是最优

6.3 收缩优化技巧

技巧 1:优先使用 map 而非 filter

// ❌ 差:filter 会导致收缩效率低下 // 因为收缩后的值可能不满足 filter 条件,被丢弃 const evenNumber = fc.integer().filter(n => n % 2 === 0); // ✅ 好:map 保证收缩后的值仍然有效 const evenNumber = fc.integer().map(n => n * 2);

原因:filter 在收缩时会丢弃不满足条件的候选值,导致收缩过程变慢甚至卡住。map 则保证任何收缩后的值经过变换后仍然有效。

技巧 2:使用 chain 处理依赖约束

// ❌ 差:生成后过滤,收缩效率低 const dateRange = fc .tuple(fc.date(), fc.date()) .filter(([a, b]) => a < b); // ✅ 好:用 chain 确保约束在生成时就满足 const dateRange = fc.date().chain(start => fc.date({ min: start }).map(end => ({ start, end })) );

技巧 3:为复杂类型提供自定义收缩提示

Hypothesis 示例

from hypothesis import given, settings, HealthCheck from hypothesis import strategies as st # 使用 @example 提供已知的重要边界值 # 这些值会在随机测试之前先被测试 @given(st.lists(st.integers())) @settings( max_examples=1000, suppress_health_check=[HealthCheck.too_slow], database=None, # 禁用数据库缓存以获得更好的收缩 ) def test_sort_preserves_length(xs): assert len(sorted(xs)) == len(xs)

技巧 4:proptest 中的自定义策略收缩

use proptest::prelude::*; // 自定义策略:生成有效的 HTTP 状态码 fn http_status_code() -> impl Strategy<Value = u16> { prop_oneof![ // 常见状态码(高权重,更容易收缩到这些值) 3 => Just(200u16), 3 => Just(201u16), 3 => Just(400u16), 3 => Just(404u16), 3 => Just(500u16), // 随机有效状态码(低权重) 1 => (100u16..600u16), ] } proptest! { #[test] fn test_status_code_category(code in http_status_code()) { let category = categorize_status(code); match code { 100..=199 => prop_assert_eq!(category, "informational"), 200..=299 => prop_assert_eq!(category, "success"), 300..=399 => prop_assert_eq!(category, "redirect"), 400..=499 => prop_assert_eq!(category, "client_error"), 500..=599 => prop_assert_eq!(category, "server_error"), _ => prop_assert!(false, "unexpected status code: {}", code), } } }

6.4 收缩调试技巧

当收缩结果不理想时,可以使用以下调试方法:

fast-check

fc.assert( fc.property(myArbitrary, (input) => { // 你的属性 return someCondition(input); }), { numRuns: 1000, verbose: 2, // 显示收缩过程的详细日志 endOnFailure: true, // 第一次失败就停止 // seed: 12345, // 固定种子以复现特定失败 } );

Hypothesis

# 在 pytest 中查看收缩过程 # 运行: pytest --hypothesis-show-statistics -v @settings( max_examples=500, verbosity=Verbosity.verbose, # 显示每次尝试 ) @given(st.lists(st.integers())) def test_my_property(xs): assert my_function(xs) >= 0

7. AI 生成 PBT 的完整工作流

7.1 端到端工作流

┌─────────────────────────────────────────────────────────────┐ │ AI 辅助 PBT 工作流 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 步骤 1: 属性发现 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Spec 文档 │───▶│ AI 分析 │───▶│ 属性列表 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 2: 生成器设计 │ │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 类型定义 │───▶│ AI 设计 │───▶│ 生成器 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 3: 测试代码生成 │ │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 属性+生成器│───▶│ AI 编写 │───▶│ 测试文件 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 4: 执行与分析 │ │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 运行测试 │───▶│ AI 分析 │───▶│ 修复建议 │ │ │ │ │ │ 反例 │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘

操作步骤

步骤 1:准备 Spec 文档

确保你的项目有结构化的需求和设计文档。在 Kiro 中:

.kiro/specs/my-feature/ ├── requirements.md # 包含验收标准 ├── design.md # 包含 Correctness Properties └── tasks.md # 包含 PBT 任务

步骤 2:让 AI 发现属性

使用以下 Prompt 让 AI 从 Spec 中提取属性:

请阅读以下 requirements.md 和 design.md,识别所有可以用 Property-Based Testing 验证的属性。 对每个属性,请提供: 1. 属性名称(如 P1, P2...) 2. 自然语言描述 3. 形式化表达(使用 ∀ 量词) 4. 对应的验收标准编号 5. 推荐的测试框架 [粘贴 Spec 内容]

步骤 3:让 AI 设计生成器

基于以下属性列表和数据类型定义,为每个属性设计智能生成器。 ## 属性列表 [步骤 2 的输出] ## 数据类型 [粘贴相关类型定义] ## 要求 - 使用 [框架名] 语法 - 生成器应覆盖边界值 - 优先使用 map/chain 而非 filter - 为复杂类型提供分层生成策略

步骤 4:让 AI 生成完整测试代码

请基于以下属性和生成器,生成完整的 PBT 测试文件。 ## 属性和生成器 [步骤 2 和 3 的输出] ## 要求 - 每个测试用 JSDoc/docstring 注释说明属性 - 包含 "Validates: Requirements X.Y" 可追溯性标注 - 设置合理的 numRuns(默认 500) - 包含 seed 固定机制以便复现

步骤 5:运行测试并分析反例

# fast-check (Jest/Vitest) npx vitest run --reporter=verbose src/**/*.property.test.ts # Hypothesis (pytest) pytest --hypothesis-show-statistics -v tests/property/ # proptest (Rust) cargo test --lib -- --nocapture property_tests # rapid (Go) go test -v -run TestProperty ./...

步骤 6:AI 分析反例

当测试失败时,将反例交给 AI 分析:

PBT 测试失败,请分析以下反例并判断原因: ## 失败的属性 [属性描述] ## 反例 [框架输出的最小反例] ## 相关代码 [被测函数的实现] ## 请判断 1. 这是测试本身的问题(属性定义不正确)? 2. 这是代码的 bug(需要修复实现)? 3. 这是 Spec 的问题(需求不完整或矛盾)? 请给出你的判断理由和修复建议。

8. 反例分析与调试

8.1 反例分类决策树

当 PBT 发现反例时,需要判断问题出在哪里。以下是系统化的分类方法:

PBT 测试失败,得到反例 X ├── 反例 X 是否满足属性的前置条件? │ ├── 否 → 生成器问题:生成器产生了无效输入 │ │ 修复:调整生成器约束 │ │ │ └── 是 → 继续判断 │ │ │ ├── 手动验证:f(X) 的结果是否符合预期? │ │ ├── 不符合 → 代码 bug │ │ │ 修复:修改被测函数实现 │ │ │ │ │ └── 符合 → 继续判断 │ │ │ │ │ ├── 属性定义是否正确反映了需求? │ │ │ ├── 否 → 属性定义错误 │ │ │ │ 修复:调整属性定义 │ │ │ │ │ │ │ └── 是 → 需求本身可能有问题 │ │ │ 行动:与产品经理/用户确认需求 │ │ │ │ │ └── 是否是浮点精度问题? │ │ └── 是 → 使用近似比较 │ │ 修复:expect(a).toBeCloseTo(b) │ │ │ └── 反例是否涉及并发/时序? │ └── 是 → 可能是竞态条件 │ 修复:添加同步机制

8.2 常见反例类型及处理

反例类型典型表现根因处理方式
空输入[], "", 0未处理空值边界修复代码:添加空值检查
极大值Number.MAX_SAFE_INTEGER整数溢出修复代码:添加范围检查
Unicode"🎉", "\u0000"字符编码处理不当修复代码:正确处理 Unicode
负数-1, -Infinity未考虑负数输入修复代码或调整生成器
重复元素[1, 1, 1]去重逻辑错误修复代码:检查去重实现
精度丢失0.1 + 0.2 ≠ 0.3浮点精度使用 epsilon 比较
顺序敏感不同排列产生不同结果算法不稳定修复代码或明确排序规则

8.3 AI 辅助反例分析的 Prompt 模板

你是一位 PBT 反例分析专家。请分析以下测试失败: ## 测试属性 名称: [属性名] 描述: [自然语言描述] 形式化: [∀ x: ...] ## 反例(框架收缩后的最小反例) 输入: [反例值] 期望: [期望的输出/行为] 实际: [实际的输出/行为] ## 被测代码 ```[语言] [被测函数实现]

请分析

  1. 分类: 这是 (a) 代码 bug, (b) 测试/属性定义错误, 还是 (c) 需求/Spec 问题?
  2. 根因: 为什么这个输入导致了失败?
  3. 修复方案:
    • 如果是代码 bug,给出修复代码
    • 如果是属性错误,给出修正后的属性
    • 如果是 Spec 问题,说明需要澄清的点
  4. 回归防护: 建议添加什么额外的测试来防止回归?
### 8.4 反例复现与回归测试 发现的反例应该被固化为回归测试: **fast-check**: ```typescript // 将 PBT 发现的反例固化为具体测试 describe('回归测试(来自 PBT 反例)', () => { // 2025-06-15 PBT 发现:空数组导致除零错误 test('regression: empty array should return 0 average', () => { expect(average([])).toBe(0); }); // 2025-06-16 PBT 发现:负数价格导致总价计算错误 test('regression: negative price should be rejected', () => { expect(() => createOrder([{ price: -1, qty: 1 }])).toThrow(); }); }); // 也可以使用 fast-check 的 seed 复现 fc.assert( fc.property(myArbitrary, myProperty), { seed: 1234567890 } // 使用失败时记录的 seed );

Hypothesis

from hypothesis import given, example # Hypothesis 自动将发现的反例存入数据库 # 下次运行时会优先测试这些已知的失败输入 # 也可以手动添加 @example 装饰器 @given(st.lists(st.integers())) @example([]) # 空列表(PBT 发现的边界值) @example([0, 0, 0]) # 全零列表(PBT 发现的边界值) @example([-1, 1]) # 正负混合(PBT 发现的边界值) def test_sort_preserves_elements(xs): sorted_xs = sorted(xs) assert sorted(sorted_xs) == sorted(xs)

9. 属性模式目录

9.1 完整属性模式参考

以下是 AI 在发现属性时应参考的完整模式目录,每种模式附带多语言示例:

模式 1:往返(Roundtrip)

适用场景:序列化/反序列化、编码/解码、压缩/解压

// fast-check: JSON 序列化往返 fc.assert( fc.property(fc.anything(), (value) => { const serialized = JSON.stringify(value); const deserialized = JSON.parse(serialized); // 注意:JSON 不支持 undefined、函数等,需要约束输入 expect(deserialized).toEqual(value); }) );
# Hypothesis: Base64 编码往返 @given(st.binary()) def test_base64_roundtrip(data: bytes): encoded = base64.b64encode(data) decoded = base64.b64decode(encoded) assert decoded == data
// Go rapid: URL 编码往返 func TestURLEncodingRoundtrip(t *testing.T) { rapid.Check(t, func(t *rapid.T) { original := rapid.String().Draw(t, "input") encoded := url.QueryEscape(original) decoded, err := url.QueryUnescape(encoded) if err != nil { t.Fatal(err) } if decoded != original { t.Fatalf("roundtrip failed: %q -> %q -> %q", original, encoded, decoded) } }) }

模式 2:幂等(Idempotent)

适用场景:排序、去重、格式化、规范化、HTTP PUT/DELETE

// fast-check: 排序幂等 fc.assert( fc.property(fc.array(fc.integer()), (arr) => { const sorted1 = [...arr].sort((a, b) => a - b); const sorted2 = [...sorted1].sort((a, b) => a - b); expect(sorted2).toEqual(sorted1); }) );
# Hypothesis: HTML 清理幂等 @given(st.text()) def test_sanitize_idempotent(html: str): once = sanitize_html(html) twice = sanitize_html(once) assert once == twice

模式 3:不变量(Invariant)

适用场景:集合操作、数值计算、状态转换

// fast-check: filter 后长度不变量 fc.assert( fc.property( fc.array(fc.integer()), fc.func(fc.boolean()), // 随机谓词 (arr, predicate) => { const filtered = arr.filter(predicate); return filtered.length <= arr.length; } ) );
// Go rapid: Map 操作后键集合不变量 func TestMapKeysInvariant(t *testing.T) { rapid.Check(t, func(t *rapid.T) { m := rapid.MapOf( rapid.String(), rapid.Int(), ).Draw(t, "map") // 对所有值加 1 transformed := make(map[string]int) for k, v := range m { transformed[k] = v + 1 } // 键集合应该不变 if len(transformed) != len(m) { t.Fatal("key set changed after value transformation") } for k := range m { if _, ok := transformed[k]; !ok { t.Fatalf("key %q missing after transformation", k) } } }) }

模式 4:等价/Oracle

适用场景:优化实现 vs 参考实现、新旧版本对比

# Hypothesis: 快速排序 vs 内置排序 @given(st.lists(st.integers())) def test_quicksort_matches_builtin(xs): assert quicksort(xs) == sorted(xs)

模式 5:变形(Metamorphic)

适用场景:搜索引擎、机器学习模型、数值计算

// fast-check: 搜索结果的变形属性 // 添加更多搜索关键词不应增加结果数量 fc.assert( fc.property( fc.string({ minLength: 1 }), // 基础查询 fc.string({ minLength: 1 }), // 额外关键词 (query, extraKeyword) => { const results1 = search(query); const results2 = search(`${query} ${extraKeyword}`); // 更精确的查询结果应该 ≤ 更宽泛的查询 return results2.length <= results1.length; } ) );

模式 6:保持属性(Preservation)

适用场景:排序保持元素集合、转换保持信息量

# Hypothesis: 排序保持元素集合 @given(st.lists(st.integers())) def test_sort_preserves_elements(xs): sorted_xs = sorted(xs) assert sorted(sorted_xs) == sorted(xs) # 多重集相等 assert len(sorted_xs) == len(xs)

模式 7:交换律/结合律(Algebraic)

适用场景:数学运算、集合操作、字符串拼接

// fast-check: 集合并集的交换律 fc.assert( fc.property( fc.uniqueArray(fc.integer()), fc.uniqueArray(fc.integer()), (setA, setB) => { const unionAB = new Set([...setA, ...setB]); const unionBA = new Set([...setB, ...setA]); expect(unionAB).toEqual(unionBA); } ) );

10. 各框架的 Prompt 模板

10.1 fast-check (JavaScript/TypeScript)

请使用 fast-check 为以下 TypeScript 函数编写 Property-Based Tests。 ## 函数签名 ```typescript [粘贴函数签名和 JSDoc]

要求

  1. 使用 fc.property() 和 fc.assert() API
  2. 为每个属性创建独立的 test() 块
  3. 使用 fc.record() 生成复杂对象
  4. 设置 numRuns: 500
  5. 添加 “Validates: Requirements X.Y” 注释
  6. 优先使用 map/chain 而非 filter 构建生成器
  7. 包含边界值:空数组、空字符串、0、负数
  8. 测试文件命名为 *.property.test.ts

属性提示

请检查以下属性模式是否适用:

  • 往返(如果存在逆操作)
  • 幂等(如果是规范化/格式化操作)
  • 不变量(输出的范围/大小约束)
  • 保持(操作前后元素集合是否不变)
### 10.2 Hypothesis (Python)

请使用 Hypothesis 为以下 Python 函数编写 Property-Based Tests。

函数签名

[粘贴函数签名和 docstring]

要求

  1. 使用 @given 装饰器和 hypothesis.strategies
  2. 使用 @settings(max_examples=500) 设置运行次数
  3. 使用 @example() 添加已知的重要边界值
  4. 对复杂类型使用 @composite 策略
  5. 如果涉及状态,使用 RuleBasedStateMachine
  6. 添加 “Validates: Requirements X.Y” docstring
  7. 测试文件命名为 test_*_properties.py
  8. 与 pytest 集成,使用 pytest-hypothesis 插件

属性提示

请检查以下属性模式是否适用:

  • 往返(serialize/deserialize 对)
  • 幂等(重复操作等价)
  • 不变量(输出约束)
  • 状态机(如果涉及有状态操作)
### 10.3 Go rapid

请使用 pgregory.net/rapid 为以下 Go 函数编写 Property-Based Tests。

函数签名

[粘贴函数签名和注释]

要求

  1. 使用 rapid.Check(t, func(t *rapid.T) {…}) 模式
  2. 使用 rapid.Custom() 构建自定义生成器
  3. 使用 t.Fatal/t.Fatalf 报告失败
  4. 生成器使用 .Draw(t, “label”) 获取值
  5. 测试函数命名为 TestProperty_*
  6. 添加 “Validates: Requirements X.Y” 注释
  7. 测试文件命名为 *_property_test.go

属性提示

请检查以下属性模式是否适用:

  • 往返(encoding/decoding)
  • 幂等(sort/normalize)
  • 不变量(size/range constraints)
  • 等价(optimized vs reference implementation)
### 10.4 proptest (Rust)

请使用 proptest 为以下 Rust 函数编写 Property-Based Tests。

函数签名

[粘贴函数签名和文档注释]

要求

  1. 使用 proptest! 宏定义测试
  2. 使用 prop::strategy 组合策略
  3. 使用 prop_assert! 和 prop_assert_eq! 断言
  4. 对复杂类型使用 prop_compose! 宏
  5. 使用 prop_oneof! 生成枚举变体
  6. 添加 “Validates: Requirements X.Y” 注释
  7. 在 #[cfg(test)] 模块中编写
  8. 使用 ProptestConfig 配置运行次数

属性提示

请检查以下属性模式是否适用:

  • 往返(serde 序列化)
  • 幂等(normalize/canonicalize)
  • 不变量(类型约束、范围检查)
  • 代数属性(交换律、结合律)
--- ## 实战案例:电商订单系统的 AI 辅助 PBT ### 案例背景 一个电商平台的订单计算模块,需要验证价格计算、折扣应用、库存扣减等核心逻辑。团队使用 Kiro 的 Spec 驱动工作流,从需求文档中提取属性并生成 PBT。 ### 案例分析 #### 第 1 步:从需求提取属性 **requirements.md(节选)**: ```markdown ### Requirement: 订单价格计算 #### Acceptance Criteria AC1: WHEN 订单包含多个商品, THEN 总价等于所有商品(单价×数量)之和 AC2: WHEN 应用折扣码, THEN 折扣后价格 = 原价 × (1 - 折扣率) AC3: WHEN 折扣后价格计算完成, THEN 最终价格不低于 0 AC4: WHEN 订单创建成功, THEN 库存应减少对应数量 AC5: WHEN 订单取消, THEN 库存应恢复到订单创建前的状态

AI 分析后提取的属性:

AC属性类型形式化描述
AC1不变量∀ order: total(order) == Σ(item.price × item.qty)
AC2不变量∀ order, discount: discounted(order, d) == total(order) × (1 - d)
AC3边界∀ order, discount: finalPrice(order, d) >= 0
AC4不变量∀ order: stock_after == stock_before - Σ(item.qty)
AC5往返∀ order: cancel(create(order)).stock == original_stock

第 2 步:设计生成器

import fc from 'fast-check'; // 商品生成器 const productArb = fc.record({ id: fc.uuid(), name: fc.string({ minLength: 1, maxLength: 50 }), price: fc.integer({ min: 1, max: 10_000_00 }), // 1分 ~ 10万元(分为单位) stock: fc.integer({ min: 0, max: 10000 }), }); // 订单项生成器(数量不超过库存) const orderItemArb = (maxStock: number) => fc.record({ productId: fc.uuid(), price: fc.integer({ min: 1, max: 10_000_00 }), quantity: fc.integer({ min: 1, max: Math.max(1, maxStock) }), }); // 订单生成器 const orderArb = fc.array( orderItemArb(100), { minLength: 1, maxLength: 20 } ); // 折扣率生成器(0% ~ 100%) const discountArb = fc.integer({ min: 0, max: 100 });

第 3 步:编写属性测试

describe('订单价格计算 - Property-Based Tests', () => { /** * AC1: 总价等于所有商品(单价×数量)之和 * Validates: Requirements AC1 */ test('P1: total equals sum of item prices × quantities', () => { fc.assert( fc.property(orderArb, (items) => { const expectedTotal = items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); const actualTotal = calculateTotal(items); return actualTotal === expectedTotal; }), { numRuns: 1000 } ); }); /** * AC2: 折扣后价格 = 原价 × (1 - 折扣率) * Validates: Requirements AC2 */ test('P2: discounted price equals total × (1 - rate)', () => { fc.assert( fc.property(orderArb, discountArb, (items, discountPercent) => { const total = calculateTotal(items); const discounted = applyDiscount(total, discountPercent); const expected = Math.floor(total * (100 - discountPercent) / 100); return discounted === expected; }), { numRuns: 1000 } ); }); /** * AC3: 最终价格不低于 0 * Validates: Requirements AC3 */ test('P3: final price is never negative', () => { fc.assert( fc.property(orderArb, discountArb, (items, discountPercent) => { const total = calculateTotal(items); const finalPrice = applyDiscount(total, discountPercent); return finalPrice >= 0; }), { numRuns: 1000 } ); }); /** * AC5: 订单取消后库存恢复(往返属性) * Validates: Requirements AC5 */ test('P5: cancel(create(order)) restores stock', () => { fc.assert( fc.property(orderArb, (items) => { const initialStock = new Map( items.map(item => [item.productId, 1000]) ); // 创建订单 → 扣减库存 const afterCreate = createOrder(items, new Map(initialStock)); // 取消订单 → 恢复库存 const afterCancel = cancelOrder(afterCreate.orderId, afterCreate.stock); // 库存应该恢复到初始状态 for (const [id, qty] of initialStock) { if (afterCancel.get(id) !== qty) return false; } return true; }), { numRuns: 500 } ); }); });

第 4 步:PBT 发现的 Bug

运行测试后,PBT 发现了以下反例:

反例 1:当折扣率为 100% 时,applyDiscount 返回 -0 而非 0

  • 分类:代码 bug
  • 修复:return Math.max(0, Math.floor(total * (100 - rate) / 100))

反例 2:当订单包含同一商品的多个条目时,库存扣减重复计算

  • 分类:代码 bug
  • 修复:合并同一商品的数量后再扣减

反例 3:当商品价格为 1 分且数量为 1 时,折扣后价格向下取整为 0

  • 分类:Spec 问题——需要确认最低价格策略
  • 行动:与产品经理确认是否允许 0 元订单

案例总结

指标传统单元测试AI 辅助 PBT
编写时间2 小时(手动编写 20+ 测试用例)30 分钟(AI 生成属性和生成器)
发现的 bug0(手动测试未覆盖边界值)3(包括 1 个 Spec 问题)
测试覆盖的输入空间20 个具体值5000+ 随机值
可维护性需求变更时需逐个修改测试属性不依赖具体值,更稳定

避坑指南

❌ 常见错误

  1. 过度使用 filter 构建生成器

    • 问题:fc.integer().filter(n => isPrime(n)) 会导致大量生成的值被丢弃,测试变慢,收缩效率极低
    • 正确做法:使用 fc.integer({min: 2, max: 1000}).map(n => nthPrime(n)) 或预计算素数列表后用 fc.constantFrom(...primes)
  2. 属性定义过于宽泛

    • 问题:∀ x: f(x) != null 这样的属性几乎不能发现任何 bug
    • 正确做法:定义精确的属性,如 ∀ x: f(x).length == x.length∀ x: f(f(x)) == f(x)
  3. 忽略收缩结果直接看原始反例

    • 问题:原始反例可能包含大量无关信息,难以定位问题
    • 正确做法:始终关注框架收缩后的最小反例,它通常直接指向 bug 的根因
  4. 在属性测试中使用 mock

    • 问题:mock 会掩盖真实的交互问题,PBT 的价值在于测试真实行为
    • 正确做法:PBT 应该测试纯函数或使用真实的(内存中的)依赖
  5. numRuns 设置过低

    • 问题:默认的 100 次运行可能不足以发现低概率的边界 bug
    • 正确做法:对关键业务逻辑设置 500-1000 次,对简单属性 100-200 次即可
  6. 不固化 PBT 发现的反例

    • 问题:PBT 发现的 bug 修复后,如果不添加回归测试,可能再次引入
    • 正确做法:将每个反例转化为具体的单元测试(@example 或独立 test case)
  7. 让 AI 同时写代码和测试

    • 问题:AI 可能在代码和测试中犯同样的理解错误,导致”测试通过但逻辑错误”
    • 正确做法:先让 AI 从 Spec 生成属性测试,再让 AI(或人工)实现代码,保持测试和实现的独立性
  8. 忽略浮点精度问题

    • 问题:0.1 + 0.2 !== 0.3 导致大量虚假失败
    • 正确做法:使用整数运算(如分为单位的金额)或 epsilon 比较
  9. 生成器不覆盖边界值

    • 问题:纯随机生成可能永远不会生成 []""0 等边界值
    • 正确做法:使用 fc.oneof(fc.constant([]), fc.array(...)) 显式包含边界值
  10. 将 PBT 当作 fuzzing 使用

    • 问题:PBT 的目标是验证属性,不是发现崩溃。如果只检查”不抛异常”,那是 fuzzing 而非 PBT
    • 正确做法:每个 PBT 测试都应该有明确的属性断言,而非仅仅”不崩溃”

✅ 最佳实践

  1. 从 Spec 开始,而非从代码开始:先定义属性(“代码应该做什么”),再实现代码。这是 TDD 在 PBT 领域的自然延伸。

  2. 属性命名使用 P1, P2… 编号:便于在 Spec、测试代码和 CI 报告之间建立可追溯性。

  3. 每个属性测试只验证一个属性:不要在一个 test 中检查多个属性,这样失败时更容易定位问题。

  4. 生成器是可复用的资产:为项目的核心数据类型建立生成器库(如 generators/order.ts),在多个属性测试中复用。

  5. PBT + 单元测试互补使用:PBT 验证通用属性,单元测试验证具体的业务规则和边界值。两者不是替代关系。

  6. CI 中设置合理的超时:PBT 运行时间不确定,在 CI 中设置 5-10 分钟的超时,避免收缩过程无限运行。

  7. 定期增加 numRuns:在 nightly build 中使用更高的 numRuns(如 10000),在 PR check 中使用较低的值(如 200)以平衡速度和覆盖度。

  8. 记录 seed 以便复现:所有 PBT 框架都支持 seed 固定。在 CI 日志中记录 seed,以便复现失败。


相关资源与延伸阅读

  1. fast-check 官方文档  — JavaScript/TypeScript PBT 框架的完整 API 文档和教程,包含丰富的生成器示例和最佳实践
  2. Hypothesis 官方文档  — Python PBT 框架的权威指南,涵盖策略组合、状态机测试和数据库集成
  3. proptest Book  — Rust proptest 框架的在线书籍,详细讲解策略设计和收缩机制
  4. rapid GitHub 仓库  — Go 现代 PBT 库,README 包含完整的使用指南和示例
  5. Property-Based Testing with PropEr, Erlang, and Elixir  — Fred Hebert 的经典 PBT 书籍,虽然以 Erlang/Elixir 为例,但属性设计思想通用
  6. jqwik 用户指南  — JVM 生态 PBT 框架,与 JUnit 5 深度集成
  7. Kiro Spec 驱动开发文档  — 了解如何在 Kiro 中使用 requirements.md 和 design.md 驱动 PBT
  8. QuickCheck 论文(原始论文)  — Claessen & Hughes 2000 年的开创性论文,理解 PBT 的理论基础
  9. Choosing properties for property-based testing  — Scott Wlaschin 的经典博文,系统化讲解如何选择属性模式
  10. pbt Rust crate  — 2025 年发布的 Rust 新一代 PBT 库,在收缩能力上优于 proptest 和 quickcheck

参考来源


📖 返回 总览与导航 | 上一节:31c-需求到测试用例自动化 | 下一节:31e-测试Steering规则与反模式

Last updated on