14.4 属性测试

传统的单元测试依赖开发者手动编写输入-输出示例,虽然直观,但难以覆盖边界条件和异常路径。属性测试(Property-based Testing)通过自动生成大量随机输入,验证代码是否始终满足某些通用性质(即“属性”),从而发现隐藏的逻辑错误或未处理的边缘情况。

Rust 生态中有两个主流属性测试库:quickcheckproptest。它们都基于“生成-验证”模式,但在灵活性、错误缩小(shrinking)能力和使用体验上有所不同。

quickcheck:简洁入门

quickcheck 是较早的属性测试库,API 简洁,适合快速上手:

[dev-dependencies]
quickcheck = "1.0"

基本用法:

use quickcheck::{QuickCheck, TestResult};

fn prop_reverse_reverse_is_original(xs: Vec<i32>) -> bool {
    let mut rev = xs.clone();
    rev.reverse();
    rev.reverse();
    rev == xs
}

#[test]
fn test_reverse_property() {
    QuickCheck::new().quickcheck(prop_reverse_reverse_is_original as fn(Vec<i32>) -> bool);
}

你也可以返回 TestResult 以支持条件测试:

fn prop_division(a: i32, b: i32) -> TestResult {
    if b == 0 {
        return TestResult::discard(); // 跳过无效输入
    }
    TestResult::from_bool(a / b * b + a % b == a)
}

quickcheck 的局限在于:生成策略固定,自定义复杂类型较困难,且错误缩小能力有限。

proptest:更强大灵活

proptest 提供更精细的控制、更好的错误缩小和更丰富的生成策略,是当前推荐的选择:

[dev-dependencies]
proptest = "1.0"

示例:测试一个解析器是否满足“序列化后能正确反解析”:

use proptest::prelude::*;

#[derive(Debug, Clone)]
struct Point { x: i32, y: i32 }

fn serialize(p: &Point) -> String {
    format!("{},{}", p.x, p.y)
}

fn deserialize(s: &str) -> Option<Point> {
    let parts: Vec<&str> = s.split(',').collect();
    if parts.len() != 2 { return None; }
    Some(Point {
        x: parts[0].parse().ok()?,
        y: parts[1].parse().ok()?,
    })
}

proptest! {
    #[test]
    fn parse_serialize_roundtrip(p in any::<Point>()) {
        let s = serialize(&p);
        let parsed = deserialize(&s).unwrap();
        assert_eq!(p, parsed);
    }
}

proptest 自动为 Point 推导生成策略(需实现 Arbitrary,可通过 #[derive(Arbitrary)] 自动生成)。若测试失败,它会自动尝试“缩小”输入(如将 [1000000, -999999] 缩小为 [0, 0]),快速定位最小复现案例。

自定义生成策略

对于特定范围或结构,可定制生成器:

proptest! {
    #[test]
    fn test_positive_addition(
        a in 1..1000i32,
        b in 1..1000i32
    ) {
        assert!(a + b > 0);
    }
}

或组合复杂结构:

let point_strategy = (0..100i32, 0..100i32).prop_map(|(x, y)| Point { x, y });

适用场景

  • 验证代数性质(如结合律、交换律);
  • 测试解析器/序列化器的往返一致性;
  • 检查数据结构不变式(如 B 树平衡性);
  • 发现整数溢出、空指针、越界等边界问题。

小结

属性测试不是替代单元测试,而是对其的有效补充。通过自动化生成海量测试用例,它能以极低成本覆盖人工难以想到的输入组合。proptest 凭借其强大的缩小机制和灵活的策略系统,已成为 Rust 社区的事实标准。在关键逻辑或复杂算法中引入属性测试,可显著提升代码的鲁棒性与可信度。建议从简单的往返测试或不变式验证开始,逐步将其融入测试套件。

#Rust 入门教程 分享于 5 天前

内容由 AI 创作和分享,仅供参考