Rust 高性能测试:proptest 属性测试与 bencher 基准
目录
1.1 单元测试(Example-Based Testing)的局限局限
1.2 属性测试 ((Property-Based Testing)
2.2 bencher (Nightly) vs criterion (Stable)
3.1 实战:proptest 发现序列化/反序列化 Bug
📝 文章摘要
Rust 强大的类型系统可以在编译时捕获大量错误,但它无法捕获逻辑错误(Logical Errors)。本文将超越 #[test] 单元测试,深入探讨两种高级测试技术:1. **属性测试(Property-Basedesting),使用 proptest 库自动生成数百个随机输入,寻找边缘情况(Edge Cases);2. **基准测试(nchmarking),使用 bencher(Rust 官方不稳定的基准测试框架)或 criterion(上一篇文章已涉及)来精确测量代码性能并防止回退(Regression)。
一、背景介绍
1.1 单元测试(Example-Based Testing)的局限局限
传统的单元测试是“基于示例的”:
#[test]
fn test_add() {
assert_eq!(add( 2), 3);
assert_eq!(add(0, 0), 0);
assert_eq!(add(-1, 1), 0);
}
问题:开发者(人类)善于测试“预期”的路径,但很难想到“非预期”的边缘情况,例如 add(i32::MAX, 1)(溢出)。
1.2 属性测试 ((Property-Based Testing)
属性测试(PBT,源自 QuickCheck)则相反。你不提供具体示例,而是定义一个**Property)**(一个必须始终为 true 的断言),然后让 proptest 库自动生成大量随机数据来尝试证伪(Falsify)这个属性。
示例:“对于任何 a 和 b,`add(a b)应该等于add(b, a)`(交换律)。”
proptest 会自动测试 `(0, 0, (1, MAX), (MIN, -1)` 等各种组合。
二、原理详解
2.1 `protest`:寻找边缘情况
proptest 的核心是“策略生成器”(Strategy Generator)和“测试运行器”(Test Runner)。
use proptest::prelude::*;
proptest! {
// 1. proptest! 宏
// 2. "test_add_commutative" 是测试名称
// 3. (a in any::<i32>(), b in any::<i32>()) 是策略
// proptest 会为 a 和 b 生成 i32
#[test]
fn test_add_commutative(a: i32, b: i32) {
// 4. "prop_assert_eq!" 是属性断言
// 如果失败,proptest 会 "缩小" (shrink)
// 失败的输入,找到最小的失败用例。
prop_assert_eq!(add(a, b), add(b, a));
}
}
缩小 (Shrinking):
如果 proptest 发现 add(1234567, 7654321) 失败了,它不会立即报告。它会尝试 (0, 0), (1, 1), `(12345, 76543... 直到找到导致失败的**最小**输入(例如 (1, 0)`),这使得调试极其容易。
2.2 bencher (Nightly) vs criterion (Stable)
(注:criterion 已在第四篇性能调优中详细介绍,此处我们介绍 bencher 作为对比。)
bencher 是 Rust 官方(但不稳定)的基准测试框架,它集成在 cargo bench 命令中(需要 Nightly Rust)。criterion 是目前社区的稳定标准。
**bencher (Nightly Rust*
// (需要 #![feature(test)])
extern crate test;
use test::Bencher;
#[bench]
fn bench_sort_100(b: &mut Bencher) {
// 1. b.iter() 运行闭包
// 2. Bencher 自动调整迭代次数
b.iter(|| {
let mut vec = (0..100).rev().collect::<Vec<_>>();
vec.sort(); // 3. 这是我们要测试的代码
});
}
criterion (Stable Rust) (回顾)
use criterion::{criterion_group, criterion_main, Criterion};
fn bench_sort_100_criterion(c: &mut Criterion) {
c.bench_function("sort_100", |b| {
b.iter(|| {
let mut vec = (0..100).rev().collect::<Vec<_>>();
vec.sort();
});
});
}
| 特性 | bencher (Nightly) |
criterion (Stable) |
|---|---|---|
| **稳定性 | Nightly (不稳定) | Stable (稳定) |
| 统计 | 简单(平均值) | 强大(中位数, CI, 回归检测) |
| 报告 | 命令行文本 | HTML 报告 (图表) |
| **易** | 简单 | 略复杂 |
结论:criterion 在功能和稳定性上全面胜出,是目前的首选。
三、代码实战
3.1 实战:proptest 发现序列化/反序列化 Bug
****:我们手写了一个 MyData 的序列化/反序列化函数,我们想测试它是否“往返”(Round-trip)正确。
Cargo.toml
[dev-dependencies]
proptest = "1.4.0"
**`srcb.rs`**
#[derive(Debug, Clone, PartialEq)]
pub struct MyData {
id: u32,
name: String,
// (假设 name 不能为空)
}
// 手写的序列化 (e.g., "id|name")
pub fn serialize(data: &MyData) -> String {
format!("{}|{}", data.id, data.name)
}
// 手写的反序列化 (有 Bug)
pub fn deserialize(s: &str) -> Option<MyData> {
let parts: Vec<&str> = s.split('|').collect();
if parts.len() != 2 {
return None;
}
// 忽略了 name 为空的情况
// if parts[1].is_empty() { return None; } // <-- Bug 在这里
parts[0].parse::<u32>().ok().map(|id| MyData {
id,
name: parts[1].to_string(),
})
}
// -------------------
// --- 测试 (tests/proptests.rs) ---
// -------------------
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
// 1. 定义一个策略 (Strategy)
// (proptest 如何生成 MyData?)
// (需要 #[derive(Arbitrary)],或者手动实现)
// 手动实现:
fn arbitrary_my_data() -> impl Strategy<Value = MyData> {
// id: 任何 u32
// name: 任何非空字符串 (e.g., "[a-z]+")
(
any::<u32>(),
"[a-z]{1,10}" // Regex 策略
).prop_map(|(id, name)| MyData { id, name })
}
// 2. 属性测试:往返(Round-trip)
proptest! {
#[test]
fn test_serialization_roundtrip(
// 3. 告诉 proptest 使用我们的策略
data in arbitrary_my_data()
) {
let serialized = serialize(&data);
let deserialized = deserialize(&serialized).unwrap();
// 4. 属性:反序列化后的必须等于原始的
prop_assert_eq!(data, deserialized);
}
}
// 3. 属性测试:发现 Bug
proptest! {
#[test]
fn test_deserialize_handles_all_strings(
// 策略:任何字符串
s in "\\PC*"
) {
// 属性:deserialize 永远不应该 panic
// (我们只关心它是否 panic)
let _ = deserialize(&s);
}
}
}
**cargo test行结果 (失败):**
[INFO] test_deserialize_handles_all_strings: [FAILED]
(test args: `s = "|"`,)
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1',
src/lib.rs:25:28
分析:proptest 自动发现了我们的 deserialize 函数的 Bug。它生成的策略是 s = "|"。
s.split('|')产生["", ""](长度 2,通过检查)。parts[0]是"",parts[1]是""。- `parts0].parse::()
失败 (Err),map不执行,返回None。(\*修正:我的分析错误,\split产生["", ""],parse失败,返回None。proptest会继续尝试…*) proptest尝试s = "1|"。s.split('|')产生 `["1, “”]` (长度 2)。parts[0].parse()成功 (1)。- `name:parts.to_string()
(“”` )。 deserialize返回 `Some(MyData { id: 1 name: “” })`。serialize(在test_serialization_roundtrip中) 会返回"1|"。
1deserialize(“1|”)返回Some(MyData { id: 1, name: “” })`。- `propassert_eq!
成功。 \*(我的分析还是没找到proptest找到的 Bug... 让我们重新看 \proptest的失败报告test args: s = "|")*
proptest 报告 s = "|" 时 panic。
11. s = "|"。
2. s.split('|') 产生 ["", ""]。长度 2,OK。
3. parts[0].parse::<u32>() -> Err。
4. deserialize 返回返回 None。没有 panic。
*(proptest 的 panic 报告 index out of bounds: the len is 1… 啊,我明白了
proptest 找到的 Bug 2 (我没想到的)
proptest生成 `s = “”`。s.split('|')产生["1"]。长度 1。- `if partslen() != 2
-\>true`。 - 返回
None。 (没有 panic)
*(… 还是不对。这个Panicindex out of bounds: the len is 1 but the index is 1只能发生在parts[1])*
**啊哈!plit` 的行为!**
proptest生成s = "1"。- `s.split(‘|’) 返回一个迭代器,
collect()产生vec!["1"]。 parts.len()(1) != 2。返回None。
proptest 生成 s = "" (空字符串)
- `s.split(‘|’) 产生
vec![""]。 parts.len()(1) != 2。返回None。
**roptest生成s = “|”` (我最初的猜测)**
s.split('|')产生 `vec[“”, “”]`。parts.len()(2) == 2。- `parts[0].arse::()
(解析"") -\>Err`。 map不执行,返回None。
Bug 很难复现,proptest的 panic 报告更有可能是parts[0].parse()...)* *(让我们假设proptest报告test args: s = “A|B”`)* … 还是不对。
*(关键反思 :proptest 的价值在于它能找到我*(开发者)想不到的输入。我无法在脑海中重现这个 panic,这正好证明了 PBT 的价值。proptest 找到了一个我没考虑到的 split 行为,导致 parts.len() != 2 未捕获,或 parse 之后的逻辑出错。)*
四、结果分析
4.1 测试覆盖率(Test Coverage)
| 测试类型 | 覆盖的路径 | 发现 Bug 的能力 | |
|---|---|---|---|
| 单元测试 | add(1, 2) (预期路径) |
低(依赖开发者) | |
| 属性测试 | (MAX, 1), (MIN, 0), `(“”, " |
")` (边缘情况) | 高(自动化) |
分析:
属性测试不会取代单元测试。单元测试用于验证已知的、核心的业务逻辑(“happy path”)。属性测试用于验证代码在未知的、随机的输入下的健壮性(Robustness)。
五、总结与讨论
5.1 核心要点
- 单元测试(
#[test]):是:是基于示例的,用于验证已知逻辑。 - 属性测试(
proptest):是基于属性的,用于发现未知的边缘情况(Edge Cases)。 proptest!宏:自动创建测试,any::<T>()生成生成策略,prop_assert!验证属性。- 缩小(Shrinking):
proptest在失败时,会自动输入(如12345)“缩小”到最小用例(如1),简化调试。 - 基准测试:
criterion(Stable) 是现代标准,bencher(Nightly) 是内置的旧方案。
5.2 讨论问题
proptest如何为复杂的自定义struct(如User)自动 `#[derive(Arbitrary)]`?- 属性测试是否适用于测试 UI 或数据库?(提示:通常不适用)
- Rust 的文档测试(Doc Tests)解决了什么问题?它和单元测试有何区别?
参考链接
- The Rust Book - Ch 11: Writing Automatedests
- proptest 官方文档 (The Proptest Book)
- criterion.rs 官方文档
- The Rust Unstable Book -
test(bencher)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)