如何使用 proptest 生成随机输入?
解读
在国内 Rust 后端、区块链、云原生等岗位的面试中,“如何写测试” 已从加分项变成必答题。
proptest 是 Rust 生态主流的**属性测试(property-based testing)框架,与 QuickCheck 相比,它能在失败时自动收缩(shrink)**输入并保留种子,方便复现。
面试官问“怎么用”,表面看是 API 调用,实则考察三点:
- 是否理解**“属性”与“随机输入”**的关系;
- 能否针对业务场景自定义 Strategy;
- 是否知道失败重现、回归测试的闭环流程。
回答时只要给出最小可运行示例 → 自定义策略 → 失败复现三段式,就能拿到高分。
知识点
- proptest::prelude:: 宏入口*:自动引入
prop_assert!等宏; - Strategy 特质:一切随机数据源的抽象,内置
any::<T>()、prop::collection::vec、prop::string::regex等; - prop_compose! 宏:把多个 Strategy 组合成复杂领域值;
- TestCase::new() 与失败文件:失败时把种子写进
proptest-regressions目录,CI 可自动回归; - #[should_panic] 与 proptest 混用时要关闭 fork 模式,避免 double panic;
- no_std 环境:可关闭默认特性
std,用alloc+ 自定义 RNG; - 性能调优:
proptest_config里调整cases、max_shrink_iters,在 CI 与本地差异化运行。
答案
use proptest::prelude::*;
// 1. 内置 Strategy:随机整数对
proptest! {
#[test]
fn test_abs_plus_abs_ge_abs_sum(a: i32, b: i32) {
// 属性:|a|+|b| >= |a+b|
prop_assert!(a.abs() + b.abs() >= (a + b).abs());
}
}
// 2. 自定义 Strategy:生成合法的中国大陆手机号
prop_compose! {
fn chinese_mobile()(tail in 0u32..=9999_9999) -> String {
let prefix = ["13", "14", "15", "16", "17", "18", "19"]
.iter()
.choose(&mut rand::thread_rng())
.unwrap();
format!("{}{:08}", prefix, tail)
}
}
proptest! {
#[test]
fn phone_must_11_digits(phone in chinese_mobile()) {
prop_assert_eq!(phone.len(), 11);
prop_assert!(phone.starts_with('1'));
}
}
// 3. 失败复现:运行一次后,proptest-regressions 目录下会生成
// test_name.toml,CI 直接 cargo test 即可回归。
cargo add proptest 后,以上代码即可通过 cargo test 运行。
若属性被违反,终端会打印最小反例与种子,开发者复制即可本地调试。
拓展思考
- 与模糊测试(fuzzing)区别:proptest 面向确定性属性,afl/libfuzzer 面向崩溃发现;二者可互补,用 proptest 写属性,再用 cargo-fuzz 做覆盖度补充。
- 状态机测试:用
proptest-state-machine可定义状态迁移模型,对区块链交易池、订单撮合等有状态系统做随机序列测试。 - 异步属性测试:在
tokio::test里使用proptest,需把proptest!宏包在Runtime::new().block_on里,或改用proptest-asynccrate。 - CI 策略:国内常用 GitHub Actions + 缓存
~/.cargo/registry,把PROPTEST_CASES环境变量设为 1000 以内,既保证覆盖又节省 Runner 时长。 - 与契约式编程结合:在函数顶部用
contractscrate 声明前置条件,再用 proptest 生成边界输入,实现**“编译期静态检查 + 运行时属性验证”** 双层防线。