#proptest #proc-macro #macro-derive #property-testing #macro #derive-debug

dev test-strategy

Procedural macro 用于轻松编写 proptest 中的高阶策略

8 个版本

0.4.0 2024年7月3日
0.3.1 2023年7月16日
0.3.0 2023年1月14日
0.2.1 2022年9月12日
0.1.0 2020年11月9日

#45 in 测试

Download history 15647/week @ 2024-04-21 12972/week @ 2024-04-28 14265/week @ 2024-05-05 14532/week @ 2024-05-12 14209/week @ 2024-05-19 13213/week @ 2024-05-26 14092/week @ 2024-06-02 14431/week @ 2024-06-09 15313/week @ 2024-06-16 16565/week @ 2024-06-23 15777/week @ 2024-06-30 17895/week @ 2024-07-07 19066/week @ 2024-07-14 19719/week @ 2024-07-21 15370/week @ 2024-07-28 21004/week @ 2024-08-04

76,159 每月下载量
74 个Crates中(直接使用68个) 使用

MIT/Apache

105KB
2K SLoC

test-strategy

Crates.io Docs.rs Actions Status

此crate提供两个过程宏,分别为 #[derive(Arbitrary)]#[proptest]

这些宏都是以下 proptest 官方宏的替代品。

test-strategy proptest proptest-derive
#[derive(Arbitrary)] #[derive(Arbitrary)]
#[proptest] proptest! { }

此crate提供的宏相对于 proptest 的官方宏有以下优势。

  • 支持高阶策略。(#[derive(Arbitrary)]#[proptest]
  • 代码格式化不会被禁用。(#[proptest]

然而,此crate宏的语法与官方宏的语法不兼容。

安装

将此添加到您的 Cargo.toml 中

[dependencies]
test-strategy = "0.4.0"
proptest = "1.5.0"

示例

您可以使用 #[derive(Arbitrary)] 自动实现 proptest 的 Arbitrary 特性。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInputStruct {
    x: u32,

    #[strategy(1..10u32)]
    y: u32,

    #[strategy(0..#y)]
    z: u32,
}

#[derive(Arbitrary, Debug)]
enum TestInputEnum {
    A,
    B,
    #[weight(3)]
    C,
    X(u32),
    Y(#[strategy(0..10u32)] u32),
}

您可以通过在函数中添加 #[proptest] 来定义属性测试。

use test_strategy::proptest;

#[proptest]
fn my_test(_x: u32, #[strategy(1..10u32)] y: u32, #[strategy(0..#y)] z: u32) {
    assert!(1 <= y && y < 10);
    assert!(z <= y);
}

属性

属性可以写在以下位置。

属性 函数 结构体 枚举 变体 字段 函数参数
#[策略]
#[任意]
#[权重]
#[映射]
#[过滤]
#[by_ref]
#[任意(参数=T)]
#[任意(界限(...))]
#[任意(转储)]
#[proptest]
#[proptest(异步= ...)]
#[proptest(转储)]

#[derive(Arbitrary)]

您可以通过在结构体或枚举声明中添加 #[derive(Arbitrary)] 来自动实现 proptest::arbitrary::Arbitrary

默认情况下,所有字段都使用通过 proptest::arbitrary::any() 获取的策略设置。

因此,以下两段代码是等效的。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    x: u32,
    y: u32,
}
use proptest::{
    arbitrary::{any, Arbitrary},
    strategy::{BoxedStrategy, Strategy},
};

#[derive(Debug)]
struct TestInput {
    x: u32,
    y: u32,
}
impl Arbitrary for TestInput {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        let x = any::<u32>();
        let y = any::<u32>();
        (x, y).prop_map(|(x, y)| Self { x, y }).boxed()
    }
}

#[策略]

您可以通过在字段中添加 #[strategy(...)] 来指定生成字段值的策略。

在以下示例中,字段 x 的值将小于 20。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    #[strategy(0..20u32)]
    x: u32,
}

#[strategy] 中,可以通过 # 后跟字段名称来使用其他字段的值。

在以下示例中,y 的值小于或等于 x

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    x: u32,
    #[strategy(0..=#x)]
    y: u32,
}

#[任意]

您不需要写 #[strategy(any_with::<Type>(expr))],可以直接写 #[any(expr)]

use proptest::collection::size_range;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug, PartialEq)]
struct TestInput {
    #[any(size_range(0..16).lift())]
    x: Vec<u16>,
}

您不需要传递给 any_with 的表达式,只需写上要更改默认值的字段值即可。

因此,以下 TestInputATestInputBTestInputC 是等效的。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInputA {
    #[any(InnerArgs { upper : 20, ..InnerArgs::default() })]
    a: Inner,
}
#[derive(Arbitrary, Debug)]
struct TestInputB {
    #[any(InnerArgs::default(), upper = 20)]
    a: Inner,
}
#[derive(Arbitrary, Debug)]
struct TestInputC {
    #[any(upper = 20)]
    a: Inner,
}

#[derive(Default)]
struct InnerArgs {
    lower: i32,
    upper: i32,
}

#[derive(Arbitrary, Debug)]
#[arbitrary(args = InnerArgs)]
struct Inner {
    #[strategy(args.lower..args.upper)]
    x: i32,
}

#[权重]

默认情况下,所有变体以相等的概率出现。

您可以在变体上添加 #[weight] 来改变变体出现的概率。

在以下示例中,TestInput::B 出现的概率是 TestInput::A 的两倍。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
enum TestInput {
    A,

    #[weight(2)]
    B,
}

如果您将 #[weight(0)] 添加到一个变体中,该变体将不会显示,因此您可以在此变体中使用不能用作 Arbitrary 的类型。

use test_strategy::Arbitrary;

#[derive(Debug)]
struct NotArbitrary;

#[derive(Arbitrary, Debug)]
enum TestInput {
    A,

    #[allow(dead_code)]
    #[weight(0)] // Removing this `#[weight(0)]` will cause a compile error.
    B(NotArbitrary),
}

#[映射]

#[strategy(...)] 中使用 prop_map 代替,可以使用 #[map(...)]

以下代码表示相同的意思。

use proptest::arbitrary::any;
use proptest::strategy::Strategy;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput1 {
    #[strategy(any::<u32>().prop_map(|x| x + 1))]
    x: u32,
}

#[derive(Arbitrary, Debug)]
struct TestInput2 {
    #[strategy(any::<u32>())]
    #[map(|x| x + 1)]
    x: u32,
}

#[derive(Arbitrary, Debug)]
struct TestInput3 {
    #[map(|x: u32| x + 1)]
    x: u32,
}

对应用于 prop_map#[map(...)] 的函数中的其他字段的引用将生成不同的策略。

#[strategy(...)] 中引用另一个字段会将它扩展为 prop_flat_map,即使它在 prop_map 中。

use proptest::arbitrary::any;
use proptest::strategy::{Just, Strategy};
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct T1 {
    x: u32,

    #[strategy(any::<u32>().prop_map(move |y| #x + y))]
    y: u32,
}
// The code above generates the following strategy.
let t1 = any::<u32>()
    .prop_flat_map(|x| (Just(x), any::<u32>().prop_map(move |y| x + y)))
    .prop_map(|(x, y)| T1 { x, y });

另一方面,如果您在 #[map] 中引用另一个字段,它将扩展为 prop_map

use proptest::arbitrary::any;
use proptest::strategy::Strategy;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct T2 {
    x: u32,

    #[map(|y: u32| #x + y)]
    y: u32,
}
// The code above generates the following strategy.
let t2 = (any::<u32>(), any::<u32>()).prop_map(|(x, y)| T2 { x, y });

如果指定给 #[map] 的函数的输入和输出类型不同,则设置在 #[strategy] 中的策略的值类型是函数的输入类型,而不是字段类型。

use proptest::arbitrary::any;
use proptest::sample::Index;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct T1 {
    #[strategy(any::<Index>())]
    #[map(|i: Index| i.index(10))]
    x: usize,
}

// `#[strategy(any::<Index>())]` can be omitted.
#[derive(Arbitrary, Debug)]
struct T2 {
    #[map(|i: Index| i.index(10))]
    x: usize,
}

#[过滤]

通过添加 #[filter],您可以限制生成的值。

在以下示例中,x 是一个偶数。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    #[filter(#x % 2 == 0)]
    x: u32,
}

您也可以在谓词中使用多个变量。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
#[filter((#x + #y) % 2 == 0)]
struct T1 {
    x: u32,
    y: u32,
}

#[derive(Arbitrary, Debug)]
struct T2 {
    x: u32,
    #[filter((#x + #y) % 2 == 0)]
    y: u32,
}

您可以使用结构或枚举的值在过滤器中使用,通过使用 #self

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
#[filter((#self.x + #self.y) % 2 == 0)]
struct TestInput {
    x: u32,
    y: u32,
}

如果为 #[filter] 指定的表达式不包含名为在其自身字段名称后附加 # 的变量,则表达式被视为谓词函数,而不是返回 bool 的表达式。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    #[filter(is_even)]
    x: u32,
}
fn is_even(x: &u32) -> bool {
    x % 2 == 0
}

#[derive(Arbitrary, Debug)]
struct T2 {
    a: u32,

    // Since `#a` exists but `#b` does not, it is treated as a predicate function.
    #[filter(|&x| x > #a)]
    b: u32,
}

同样,如果一个表达式不包含 #self,并且它附加到一个类型上,那么这个表达式被视为谓词函数。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
#[filter(is_even)]
struct T {
    x: u32,
}
fn is_even(t: &T) -> bool {
    t.x % 2 == 0
}

您可以通过将两个参数传递给 #[filter] 来指定过滤器名称。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    #[filter("x is even", #x % 2 == 0)]
    x: u32,
}

#[by_ref]

默认情况下,如果您使用带有 #[strategy]#[any]#[map]#[filter] 的变量,并且它被 # 附加,则复制的值将被设置。

#[by_ref] 添加到字段使其使用引用而不是复制的值。

use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInput {
    #[by_ref]
    #[strategy(1..10u32)]
    x: u32,

    #[strategy(0..*#x)]
    y: u32,
}

#[任意]

#[任意(参数=T)]

指定 Arbitrary::Parameters 的类型。

您可以使用此类型的 Rc 值在 #[strategy]#[any]#[filter] 中,变量名为 args

use test_strategy::Arbitrary;

#[derive(Debug, Default)]
struct TestInputArgs {
    x_max: u32,
}

#[derive(Arbitrary, Debug)]
#[arbitrary(args = TestInputArgs)]
struct TestInput {
    #[strategy(0..=args.x_max)]
    x: u32,
}

#[任意(界限(T1,T2, ..))]

默认情况下,如果未指定带有 #[strategy] 的字段的类型包含泛型参数,则该类型设置为特质界限。

因此,以下 TestInputATestInputB 是等价的。

use proptest::{
    arbitrary::any, arbitrary::Arbitrary, strategy::BoxedStrategy, strategy::Strategy,
};
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug)]
struct TestInputA<T> {
    x: T,
}

#[derive(Debug)]
struct TestInputB<T> {
    x: T,
}
impl<T: Arbitrary + 'static> Arbitrary for TestInputB<T> {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        any::<T>().prop_map(|x| Self { x }).boxed()
    }
}

带有 #[strategy] 的字段的类型不会自动设置特质界限,因此您需要使用 #[arbitrary(bound(T))] 手动设置特质界限。

use proptest::arbitrary::any_with;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug, PartialEq)]
#[arbitrary(bound(T))]
struct TestInput<T> {
    #[strategy(any_with::<T>(Default::default()))]
    x: T,
}

您也可以指定谓词而不是类型。

use proptest::arbitrary::{any_with, Arbitrary};
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug, PartialEq)]
#[arbitrary(bound(T : Arbitrary + 'static))]
struct TestInput<T> {
    #[strategy(any_with::<T>(Default::default()))]
    x: T,
}

.. 表示自动生成的特质界限。

以下示例在自动生成的特质界限之外使用手动指定的特质界限。

use proptest::arbitrary::any_with;
use test_strategy::Arbitrary;

#[derive(Arbitrary, Debug, PartialEq)]
#[arbitrary(bound(T1, ..))]
struct TestInput<T1, T2> {
    #[strategy(any_with::<T1>(Default::default()))]
    x: T1,

    y: T2,
}

#[任意(转储)]

这会导致编译错误,并将由 #[derive(Arbitrary)] 生成的代码作为错误信息输出。

#[proptest]

当定义属性测试时,使用 #[proptest] 属性来代替 #[test]

以下示例定义了一个测试,该测试接受各种整数作为输入。

use test_strategy::proptest;

#[proptest]
fn my_test(_input: i32) {
    // ...
}

您可以将 #[strategy]#[any]#[filter]#[by_ref] 添加到 # [proptest] 函数的参数中。

use test_strategy::proptest;

#[proptest]
fn my_test2(#[strategy(10..20)] _input: i32) {
    // ...
}

您可以通过将 #[proptest] 属性的参数设置为 proptest::prelude::ProptestConfig 类型值来更改属性测试的配置。

use proptest::prelude::ProptestConfig;
use test_strategy::proptest;


#[proptest(ProptestConfig { cases : 1000, ..ProptestConfig::default() })]
fn my_test_with_config(_input: i32) {
    // ...
}

#[any] 一样,您也可以只设置要更改的字段的值,而不是默认值。

以下示例与上述示例等效。

use proptest::prelude::ProptestConfig;
use test_strategy::proptest;

#[proptest(ProptestConfig::default(), cases = 1000)]
fn my_test_with_config_2(_input: i32) {
    // ...
}

#[proptest(cases = 1000)]
fn my_test_with_config_3(_input: i32) {
    // ...
}

#[proptest(异步= ...)]

可以通过将 async = ... 设置为 #[proptest] 的参数来测试异步函数。

async = 之后允许以下值。该值指定用于测试的异步运行时。

  • "tokio"
[dev-dependencies]
test-strategy = "0.4.0"
proptest = "1.5.0"
tokio = { version = "1.38.0", features = ["rt-multi-thread"] }
use test_strategy::proptest;
use proptest::prop_assert;

#[proptest(async = "tokio")]
async fn my_test_async() {
    async { }.await;
    prop_assert!(true);
}

#[proptest(转储)]

您可以使用 #[proptest(dump)] 和将 #[proptest] 生成的代码作为编译错误信息输出。

#[proptest(dump)]
fn my_test(_input: i32) {
    // ...
}

许可证

本项目采用 Apache-2.0/MIT 双重许可。有关详细信息,请参阅两个 LICENSE-* 文件。

贡献

除非您明确表示,否则根据 Apache-2.0 许可证定义的您提交的任何旨在包含在本作品中的贡献,均将按上述方式双重许可,而不附加任何额外条款或条件。

依赖关系

~0.3–0.8MB
~19K SLoC