Rust属性测试库proptest-macro的使用,自动化生成随机输入数据的属性测试宏

Rust属性测试库proptest-macro的使用,自动化生成随机输入数据的属性测试宏

安装

在你的项目目录中运行以下Cargo命令:

cargo add proptest-macro

或者在Cargo.toml中添加以下行:

proptest-macro = "0.2.0"

使用示例

proptest-macro是一个Rust属性宏,用于简化属性测试的编写,它可以自动生成随机输入数据。下面是一个完整的使用示例:

use proptest::prelude::*;
use proptest_macro::proptest;

// 定义一个简单的函数,我们想测试它
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 使用proptest宏自动生成测试用例
#[proptest]
fn test_add(a: i32, b: i32) {
    // 测试加法交换律
    prop_assert_eq!(add(a, b), add(b, a));
    // 测试加法结合律
    prop_assert_eq!(add(add(a, b), b), add(a, add(b, b)));
}

// 可以限制输入范围
#[proptest]
fn test_add_with_range(#[strategy(1..100)] a: i32, #[strategy(1..100)] b: i32) {
    let result = add(a, b);
    prop_assert!(result >= 2 && result < 200);
}

// 测试字符串操作
fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

#[proptest]
fn test_string_reversal(#[filter(#s.len() > 0)] s: String) {
    let reversed = reverse_string(&s);
    prop_assert_eq!(reverse_string(&reversed), s);
}

功能说明

  1. #[proptest] 宏会自动为测试函数生成随机输入数据
  2. 可以使用 #[strategy] 属性限制输入数据的范围
  3. 可以使用 #[filter] 属性过滤不符合条件的输入
  4. 测试函数中使用 prop_assert!prop_assert_eq! 进行断言

高级用法

use proptest::collection::vec;
use proptest::prelude::*;
use proptest_macro::proptest;

// 测试向量排序
fn sort_vector(mut v: Vec<i32>) -> Vec<i32> {
    v.sort();
    v
}

#[proptest]
fn test_sort(
    #[strategy(vec(any::<i32>(), 0..100))] v: Vec<i32>
) {
    let sorted = sort_vector(v.clone());
    // 检查长度不变
    prop_assert_eq!(sorted.len(), v.len());
    // 检查排序后相邻元素有序
    for i in 0..sorted.len().saturating_sub(1) {
        prop_assert!(sorted[i] <= sorted[i+1]);
    }
    // 检查包含相同元素
    prop_assert_eq!(sorted.iter().sum::<i32>(), v.iter().sum::<i32>());
}

完整示例demo

下面是一个更完整的示例,展示了proptest-macro的各种用法:

use proptest::prelude::*;
use proptest_macro::proptest;
use proptest::collection::{vec, btree_set};

// 示例1: 测试平方函数
fn square(x: i32) -> i32 {
    x * x
}

#[proptest]
fn test_square(#[strategy(0..=1000)] x: i32) {
    let result = square(x);
    prop_assert!(result >= 0);
    prop_assert_eq!(result, x * x);
    if x > 46340 {  // 检查整数溢出
        prop_assert!(result < 0);
    }
}

// 示例2: 测试集合操作
fn deduplicate(vec: Vec<i32>) -> Vec<i32> {
    let mut set = btree_set(vec);
    set.into_iter().collect()
}

#[proptest]
fn test_deduplicate(
    #[strategy(vec(any::<i32>(), 0..100))] input: Vec<i32>
) {
    let output = deduplicate(input.clone());
    // 检查输出长度小于等于输入长度
    prop_assert!(output.len() <= input.len());
    // 检查输出已排序
    for i in 0..output.len().saturating_sub(1) {
        prop_assert!(output[i] <= output[i+1]);
    }
    // 检查输出包含所有唯一元素
    let input_set: std::collections::BTreeSet<_> = input.iter().collect();
    let output_set: std::collections::BTreeSet<_> = output.iter().collect();
    prop_assert_eq!(input_set, output_set);
}

// 示例3: 测试自定义类型
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn distance(&self) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }
}

#[proptest]
fn test_point_distance(
    #[strategy(-1000..1000)] x: i32,
    #[strategy(-1000..1000)] y: i32,
) {
    let point = Point { x, y };
    let distance = point.distance();
    prop_assert!(distance >= 0.0);
    prop_assert!(distance <= ((1000f64.powi(2) * 2.0).sqrt());
}

// 示例4: 使用过滤器
fn is_palindrome(s: &str) -> bool {
    s.chars().eq(s.chars().rev())
}

#[proptest]
fn test_palindrome(
    #[filter(#s.len() > 0 && #s.chars().all(|c| c.is_alphabetic()))] 
    #[strategy("[a-zA-Z]{1,20}")] 
    s: String
) {
    let reversed = s.chars().rev().collect::<String>();
    prop_assert_eq!(is_palindrome(&s), s == reversed);
    if is_palindrome(&s) {
        prop_assert_eq!(s, reversed);
    }
}

这个库是proptest框架的一部分,可以帮助你编写更全面的属性测试,自动生成各种边界情况和随机输入,提高代码的健壮性。


1 回复

Rust属性测试库proptest-macro的使用指南

简介

proptest-macro是Rust属性测试库proptest的一个宏扩展,它允许开发者通过属性宏的方式快速编写属性测试,自动生成随机输入数据来验证代码的正确性。

安装

在Cargo.toml中添加依赖:

[dependencies]
proptest = "1.0"
proptest-derive = "0.3"

基本用法

1. 简单属性测试

use proptest::prelude::*;
use proptest_derive::Arbitrary;

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

proptest! {
    #[test]
    fn test_point_coordinates(point in any::<Point>()) {
        // 测试随机生成的Point结构体
        prop_assert!(point.x.abs() <= 1000);
        prop_assert!(point.y.abs() <= 1000);
    }
}

2. 自定义生成策略

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_string_concat(
        a in "[a-z]{1,10}",  // 生成1-10个小写字母
        b in "[A-Z]{1,5}",   // 生成1-5个大写字母
    ) {
        let concat = format!("{}{}", a, b);
        prop_assert_eq!(concat.len(), a.len() + b.len());
        prop_assert!(concat.starts_with(&a));
        prop_assert!(concat.ends_with(&b));
    }
}

3. 集合测试

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_vec_operations(
        vec in prop::collection::vec(0..1000, 1..100) // 生成1-100个元素的vec,元素值0-1000
    ) {
        prop_assert!(!vec.is_empty());
        let len = vec.len();
        let first = vec[0];
        let last = vec[len - 1];
        
        // 测试一些向量操作
        let mut sorted = vec.clone();
        sorted.sort();
        prop_assert_eq!(sorted[0], *vec.iter().min().unwrap());
        prop_assert_eq!(sorted[len-1], *vec.iter().max().unwrap());
    }
}

高级用法

1. 自定义类型生成

use proptest::prelude::*;
use proptest_derive::Arbitrary;

#[derive(Debug, Arbitrary)]
enum HttpStatus {
    #[weight(70)]  // 70%概率生成成功状态码
    Success(#[strategy(200..300)] u16),
    #[weight(20)]  // 20%概率生成客户端错误
    ClientError(#[strategy(400..500)] u16),
    #[weight(10)]  // 10%概率生成服务端错误
    ServerError(#[strategy(500..600)] u16),
}

proptest! {
    #[test]
    fn test_http_status(status in any::<HttpStatus>()) {
        match status {
            HttpStatus::Success(code) => prop_assert!(code >= 200 && code < 300),
            HttpStatus::ClientError(code) => prop_assert!(code >= 400 && code < 500),
            HttpStatus::ServerError(code) => prop_assert!(code >= 500 && code < 600),
        }
    }
}

2. 过滤和修改生成器

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_even_numbers(
        // 生成偶数,过滤掉奇数
        even in (0..1000u32).prop_filter("values must be even", |x| x % 2 == 0)
    ) {
        prop_assert_eq!(even % 2, 0);
    }
}

3. 组合测试

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_string_operations(
        (a, b) in (
            "[a-z]{1,5}",  // 第一个字符串
            "[0-9]{1,3}",  // 第二个字符串
        )
    ) {
        let combined = format!("{}-{}", a, b);
        prop_assert!(combined.contains(&a));
        prop_assert!(combined.contains(&b));
        prop_assert_eq!(combined.len(), a.len() + b.len() + 1);
    }
}

配置测试

可以通过proptest的配置来控制测试行为:

use proptest::prelude::*;

proptest! {
    #![proptest_config(ProptestConfig {
        cases: 1000,       // 运行1000个测试用例
        max_shrink_rounds: 1000, // 最大缩减轮数
        ..ProptestConfig::default()
    })]
    
    #[test]
    fn test_with_config(input in 0..1000i32) {
        prop_assert!(input >= 0 && input < 1000);
    }
}

最佳实践

  1. 确定性测试:确保测试是确定性的,给定相同的随机种子应该产生相同的结果
  2. 缩小失败用例:proptest会自动尝试缩小导致测试失败的输入
  3. 合理过滤:避免过度过滤生成的输入,这会影响测试效率
  4. 组合简单策略:通过组合简单策略来构建复杂输入
  5. 属性设计:思考"对于所有输入,什么属性必须成立",而不仅仅是特定用例

proptest-macro通过简化属性测试的编写,使得开发者能够更容易地为代码添加基于随机输入的测试,从而提高代码的健壮性和可靠性。

完整示例代码

下面是一个完整的proptest-macro使用示例,演示了如何测试一个简单的字符串反转函数:

use proptest::prelude::*;

// 要测试的函数:反转字符串
fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

proptest! {
    #[test]
    fn test_string_reversal(
        input in "\\PC*"  // 生成任意可打印字符的字符串
    ) {
        let reversed = reverse_string(&input);
        prop_assert_eq!(input.len(), reversed.len());
        
        // 反转两次应该得到原始字符串
        let double_reversed = reverse_string(&reversed);
        prop_assert_eq!(input, double_reversed);
        
        // 检查反转后的字符串确实与原始字符串相反
        if !input.is_empty() {
            prop_assert_eq!(input.chars().next(), reversed.chars().last());
            prop_assert_eq!(input.chars().last(), reversed.chars().next());
        }
    }
}

#[derive(Debug, Arbitrary)]
struct Name {
    first: String,
    last: String,
}

proptest! {
    #[test]
    fn test_name_formatting(name in any::<Name>()) {
        // 测试名字格式化功能
        let formatted = format!("{}, {}", name.last, name.first);
        prop_assert!(formatted.contains(&name.first));
        prop_assert!(formatted.contains(&name.last));
        prop_assert!(formatted.starts_with(&name.last));
    }
}

这个完整示例展示了:

  1. 测试一个字符串反转函数的基本属性
  2. 使用自定义生成策略生成任意可打印字符
  3. 验证反转操作的各种属性
  4. 使用自定义结构体进行测试
  5. 验证格式化字符串的属性
回到顶部