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);
}
功能说明
#[proptest]
宏会自动为测试函数生成随机输入数据- 可以使用
#[strategy]
属性限制输入数据的范围 - 可以使用
#[filter]
属性过滤不符合条件的输入 - 测试函数中使用
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);
}
}
最佳实践
- 确定性测试:确保测试是确定性的,给定相同的随机种子应该产生相同的结果
- 缩小失败用例:proptest会自动尝试缩小导致测试失败的输入
- 合理过滤:避免过度过滤生成的输入,这会影响测试效率
- 组合简单策略:通过组合简单策略来构建复杂输入
- 属性设计:思考"对于所有输入,什么属性必须成立",而不仅仅是特定用例
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));
}
}
这个完整示例展示了:
- 测试一个字符串反转函数的基本属性
- 使用自定义生成策略生成任意可打印字符
- 验证反转操作的各种属性
- 使用自定义结构体进行测试
- 验证格式化字符串的属性