如何使用 proptest 生成随机输入?

解读

在国内 Rust 后端、区块链、云原生等岗位的面试中,“如何写测试” 已从加分项变成必答题。
proptest 是 Rust 生态主流的**属性测试(property-based testing)框架,与 QuickCheck 相比,它能在失败时自动收缩(shrink)**输入并保留种子,方便复现。
面试官问“怎么用”,表面看是 API 调用,实则考察三点:

  1. 是否理解**“属性”“随机输入”**的关系;
  2. 能否针对业务场景自定义 Strategy
  3. 是否知道失败重现、回归测试的闭环流程。
    回答时只要给出最小可运行示例 → 自定义策略 → 失败复现三段式,就能拿到高分。

知识点

  • proptest::prelude:: 宏入口*:自动引入 prop_assert! 等宏;
  • Strategy 特质:一切随机数据源的抽象,内置 any::<T>()prop::collection::vecprop::string::regex 等;
  • prop_compose! 宏:把多个 Strategy 组合成复杂领域值
  • TestCase::new() 与失败文件:失败时把种子写进 proptest-regressions 目录,CI 可自动回归;
  • #[should_panic] 与 proptest 混用时要关闭 fork 模式,避免 double panic;
  • no_std 环境:可关闭默认特性 std,用 alloc + 自定义 RNG;
  • 性能调优proptest_config 里调整 casesmax_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 运行。
若属性被违反,终端会打印最小反例与种子,开发者复制即可本地调试。

拓展思考

  1. 与模糊测试(fuzzing)区别:proptest 面向确定性属性,afl/libfuzzer 面向崩溃发现;二者可互补,用 proptest 写属性,再用 cargo-fuzz 做覆盖度补充。
  2. 状态机测试:用 proptest-state-machine 可定义状态迁移模型,对区块链交易池、订单撮合等有状态系统做随机序列测试。
  3. 异步属性测试:在 tokio::test 里使用 proptest,需把 proptest! 宏包在 Runtime::new().block_on 里,或改用 proptest-async crate。
  4. CI 策略:国内常用 GitHub Actions + 缓存 ~/.cargo/registry,把 PROPTEST_CASES 环境变量设为 1000 以内,既保证覆盖又节省 Runner 时长。
  5. 与契约式编程结合:在函数顶部用 contracts crate 声明前置条件,再用 proptest 生成边界输入,实现**“编译期静态检查 + 运行时属性验证”** 双层防线。