#属性测试 #模糊测试 #快速检查 #测试框架 #属性 #模糊

dicetest

使用随机生成的测试数据编写测试的框架

5个不稳定版本

0.3.1 2022年2月27日
0.3.0 2021年9月10日
0.2.1 2020年12月5日
0.2.0 2020年9月27日
0.1.0 2019年1月12日

#368 in 测试

Download history 1/week @ 2024-04-07 28/week @ 2024-06-30 63/week @ 2024-07-07 117/week @ 2024-07-14 95/week @ 2024-07-21

303 每月下载量
用于 diceprop

MIT/Apache

305KB
6.5K SLoC

dicetest

使用随机生成的测试数据编写测试的框架。

此crate的状态

作者认为这个crate还不够稳定。更改将在变更日志中记录。

示例

以下是一个使用dicetest测试的错误排序函数的示例

fn bubble_sort<T: Ord>(slice: &mut [T]) {
    let len = slice.len();

    for _ in 0..len {
        for j in 1..len - 1 {
            let jpp = j + 1;
            if slice[j] > slice[jpp] {
                slice.swap(j, jpp);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use dicetest::prelude::*;

    #[test]
    fn result_of_bubble_sort_is_sorted() {
        Dicetest::repeatedly().run(|mut fate| {
            let mut v = fate.roll(dice::vec(dice::u8(..), ..));
            hint!("unsorted: {:?}", v);

            bubble_sort(&mut v);
            hint!("  sorted: {:?}", v);

            let is_sorted = v.windows(2).all(|w| w[0] <= w[1]);
            assert!(is_sorted);
        })
    }
}

运行 cargo test 产生以下输出

The test failed after 31 passes.

# Config
- seed: 3713861809241954222
- start limit: 0
- end limit: 100
- passes: 200

# Counterexample
- run code: "/yiA1sab3S4UnCf4ozyMpxMxzg1NtFybCuYLHy0/oscDAAAAAAAAAA=="
- limit: 3
- hints:
    - unsorted: [201, 209, 2]
    -   sorted: [201, 2, 209]
- error: assertion failed: is_sorted

您可以通过设置环境变量来重新运行反例

DICETEST_DEBUG=/yiA1sab3S4UnCf4ozyMpxMxzg1NtFybCuYLHy0/oscDAAAAAAAAAA== cargo test

或者您也可以修改测试

Dicetest::debug("/yiA1sab3S4UnCf4ozyMpxMxzg1NtFybCuYLHy0/oscDAAAAAAAAAA==").run(|mut fate| {
    // ...
})

功能

以下功能是可用的

  • 许多libstd类型的生成器(u8StringVec等)。
  • 函数的生成器(FnMutFnOnceFn)。
  • 生成器组合器(mapflat_mapzip等)。
  • rand::distributions::Distribution的集成。
  • quickcheck::Arbitrary的集成(不包含缩小)。
  • 可配置的测试运行器。
  • 测试调试工具(hintsstats)。

以下功能是缺少的

  • 反例缩小。
  • 自定义伪随机数生成器。
  • 为任意类型创建自己的类型类。

替代方案

  • 写下您的测试数据并使用循环。
  • 使用crate quickcheck
  • 使用crate proptest

指南

本节将指导您了解dicetest最重要的概念和功能。

伪随机性

类型 Seed 允许确定 伪随机数。您可以使用固定的 Seed 或随机的 Seed

use dicetest::Seed;

println!("{:?}", Seed(42));
// Output: Seed(42)

println!("{:?}", Seed::random());
// Output: Seed(8019292413750407764)

Seed 可以用于初始化 伪随机数生成器 Prng。对于每个 SeedPrng 都提供了一组不同的无限伪随机序列的 u64

use dicetest::{Prng, Seed};

fn print_random_values(mut prng: Prng) {
    for _ in 0..3 {
        print!("{:?}, ", prng.next_number());
    }
    println!("...");
}

print_random_values(Prng::from_seed(Seed(42)));
// Output: 16628028624323922065, 3476588890713931039, 59688652182557721, ...
print_random_values(Prng::from_seed(Seed(42)));
// Output: 16628028624323922065, 3476588890713931039, 59688652182557721, ...
print_random_values(Prng::from_seed(Seed::random()));
// Output: 4221507577048064061, 15374206214556255352, 4977687432463843847, ...
print_random_values(Prng::from_seed(Seed::random()));
// Output: 11086225885938422405, 9312304973013875005, 1036200222843160301, ...

骰子

尽管 Prng 只能生成伪随机 u64,但 u64 可以用于构建更复杂的数据。特性 DieOnceDie 代表基于 Prng 的任何类型的值生成器。

DieOnce 的实现者是一个只能使用一次的生成器(类似于 FnOnce)。

use dicetest::prelude::*;

let xx = "xx".to_string();
let yy = "yy".to_string();

// This generator implements `DieOnce`.
// It chooses one of the `String`s without cloning them.
let xx_or_yy_die = dice::one_of_once().two(xx, yy);

Die 的实现者是一个可以无限次使用的生成器(类似于 Fn)。

use dicetest::prelude::*;

let xx = "xx".to_string();
let yy = "yy".to_string();

// This generator implements `Die`.
// It chooses one of the `String`s by cloning them.
let xx_or_yy_die = dice::one_of().two(xx, yy);

// This generator uses `xx_or_yy_die` to generate three `String`s at once.
let three_xx_or_yy_die = dice::array::<_, _, 3>(xx_or_yy_die);

生成器可以轻松实现和组合

use dicetest::prelude::*;

// A classic die that generates a number between 1 and 6 with uniform distribution.
let classic_die = dice::one_of().six::<u8>(1, 2, 3, 4, 5, 6);

// A loaded die that generates the number 6 more frequently.
let loaded_die =
    dice::weighted_one_of().six::<u8>((1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 6));

// This die generates the result of the function.
let die_from_fn = dice::from_fn(|_| 42);

// This die generates always the same `String` by cloning the original one.
let foo_die = dice::just("foo".to_string());

// This die generates an arbitrary byte.
let byte_die = dice::u8(..);

// This die generates a non-zero byte.
let non_zero_byte_die = dice::u8(1..);

// This die generates a `Vec` that contains an arbitrary number of arbitrary bytes.
let bytes_die = dice::vec(dice::u8(..), ..);

// This die generates a `Vec` that contains up to 10 arbitrary bytes.
let up_to_ten_bytes_die = dice::vec(dice::u8(..), ..=10);

// This die generates an arbitrary wrapped byte.
struct WrappedByte(u8);
let wrapped_byte_die = dice::u8(..).map(WrappedByte);

// This die generates a permutation of `(0..=n)` for an arbitrary `n`.
let permutation_die = dice::length(0..).flat_map(|n| {
    let vec = (0..=n).collect::<Vec<_>>();
    dice::shuffled_vec(vec)
});

结构体 Fate 是使用 DieOnceDie 所必需的。它包含两个参数

  • Prng:为 DieOnceDie 的实现者提供伪随机 u64,以构建更复杂的数据。实现者应仅将其用作其随机性的来源。
  • Limit:实现者生成的动态数据结构长度的上限。实现者可以自由解释或甚至忽略此值。
use dicetest::prelude::*;
use dicetest::{Limit, Prng};

// Provides the randomness for the generator and will be mutated when used.
let mut prng = Prng::from_seed(0x5EED.into());
// Limits the length of dynamic data structures. The generator has only read access.
let limit = Limit(5);

// Contains all parameters necessary for using `DieOnce` or `Die`.
let mut fate = Fate::new(&mut prng, limit);

// Generator for a `Vec` with an arbitrary length.
let vec_die = dice::vec(dice::u8(..), ..);

// Generates a `Vec`. Although `vec_die` can generate a `Vec` with an arbitrary length,
// the length of the actual `Vec` is limited by `limit`.
let vec = fate.roll(vec_die);
assert!(vec.len() <= 5);

println!("{:?}", vec);
// Output: [252, 231, 153, 0]

测试

如果您想编写使用随机生成的测试数据的测试,可以使用测试构建器 Dicetest

  • 它可以通过源代码或环境变量进行配置。
  • 它使用不同的种子重复运行您的测试。
  • 它记录有用的信息,有助于您调试测试。
use dicetest::prelude::*;

#[test]
fn test_foo() {
    // Runs your test with default configuration.
    Dicetest::repeatedly().run(|fate| {
        // Write your test here.
    });
}

#[test]
fn test_bar() {
    // Runs your test with custom configuration.
    Dicetest::repeatedly().passes(10000).run(|fate| {
        // Write your test here.
    });
}

闭包包含您的测试。使用传递的 fate,您可以生成测试数据并进行断言。如果闭包崩溃,Dicetest 会捕获崩溃,将测试结果记录到 stdout 并恢复崩溃。

提示

提示可以用于分析单个测试运行。在大多数情况下,您想分析反例。使用它来揭示哪些测试数据被生成或哪些分支被采取

use dicetest::prelude::*;

#[test]
fn test_foo() {
    Dicetest::repeatedly().run(|mut fate| {
        let x = fate.roll(dice::u8(1..=5));
        hint_debug!(x);

        let y = fate.roll(dice::u8(1..=3));
        if y != x {
            hint!("took branch if with y = {}", y);

            assert_eq!(3, y);
        } else {
            hint!("took branch else");
        }
    })
}

运行测试产生以下输出

The test failed after 0 passes.

# Config
- seed: 10929669535587280453
- start limit: 0
- end limit: 100
- passes: 200

# Counterexample
- run code: "JfXG0LRXjKUMu+YmdrF38/GstRdeLAeMRTKskCQcgNoAAAAAAAAAAA=="
- limit: 0
- hints:
    - x = 5
    - took branch if with y = 1
- error: assertion failed: `(left == right)`
  left: `3`,
 right: `1`

统计数据

统计数据可以用于分析多个测试运行。使用它来揭示生成测试数据的分布或分支的概率

use dicetest::prelude::*;

#[test]
fn test_foo() {
    Dicetest::repeatedly().run(|mut fate| {
        let x = fate.roll(dice::u8(1..=5));
        stat_debug!(x);

        let y = fate.roll(dice::u8(1..=3));
        if y != x {
            stat!("branch", "if with y = {}", y)
        } else {
            stat!("branch", "else");
        }
    })
}

使用环境变量 DICETEST_STATS_ENABLED=true 运行测试产生以下输出

The test withstood 200 passes.

# Config
- seed: 5043079553183914912
- start limit: 0
- end limit: 100
- passes: 200

# Stats
- branch:
    - 29.50% (59): if with y = 1
    - 27.50% (55): if with y = 3
    - 22.50% (45): if with y = 2
    - 20.50% (41): else
- x:
    - 31.50% (63): 1
    - 22.00% (44): 5
    - 17.00% (34): 2
    - 15.50% (31): 4
    - 14.00% (28): 3

环境变量

您可以使用环境变量来配置测试而无需更改源代码。有关受支持的环境变量的完整列表,请参阅 Dicetest 的文档。以下是一些示例

  • 您想使用其运行代码(从测试结果复制)调试 mytest 的反例
DICETEST_DEBUG=ABIDje/+CYVkmmCVTwKJ2go6VrzZWMjO2Bqc9m3b3h0DAAAAAAAAAA== cargo test mytest
  • 您想用其种子(从测试结果复制)重现 mytest 的结果
DICETEST_SEED=795359663177100823 cargo test mytest
  • 您想查看 mytest 的统计数据
DICETEST_STATS_ENABLED=true cargo test -- --show-output mytest
  • 您想用更多的迭代次数和更大的测试数据运行 mytest
DICETEST_PASSES_MULTIPLIER=10 DICETEST_LIMIT_MULTIPLIER=2 cargo test mytest
  • 您想用单个测试运行运行 mytest 并查看测试结果
DICETEST_MODE=once cargo test -- --show-output mytest

功能标志

存在几个功能标志,可以在编译时禁用运行时开销或启用附加功能。

hints(默认启用)

在编译时启用或禁用提示功能。如果禁用,所有提示操作都是无操作。

stats(默认启用)

在编译时启用或禁用统计功能。如果禁用,所有统计操作都是无操作。

rand_core(默认禁用)

如果启用,dicetest::Prngdicetest::Fate 实现了 rand_core::RngCore 特性。

rand_full(默认禁用,为 rand_core,rand 的别名)

如果启用,Fate::roll_distributiondice::from_distribution 可用。这允许从 rand::distributions::Distribution 的实现生成值和创建 Die

use dicetest::prelude::*;
use dicetest::{Limit, Prng};

let mut prng = Prng::from_seed(0x5EED.into());
let limit = Limit(5);
let mut fate = Fate::new(&mut prng, limit);

// Generate a value from a `rand::distributions::Distribution`
let byte: u8 = fate.roll_distribution(rand::distributions::Standard);
println!("{:?}", byte);
// Output: 28

// Create a `Die` from a `rand::distributions::Distribution`
let byte_die = dice::from_distribution(rand::distributions::Standard);
let bytes_die = dice::vec(byte_die, 1..);
let bytes: Vec<u8> = fate.roll(bytes_die);
println!("{:?}", bytes);
// Output: [236, 205, 151, 229]

quickcheck_full(默认禁用,为 rand_core,quickcheck 的别名)

如果启用,Fate 实现了 quickcheck::Gen 特性,并且 Fate::roll_arbitrarydice::arbitrary 可用。这允许为实现了 quickcheck::Arbitrary 的类型生成值和创建 Die

use dicetest::prelude::*;
use dicetest::{Limit, Prng};

let mut prng = Prng::from_seed(0x5EED.into());
let limit = Limit(5);
let mut fate = Fate::new(&mut prng, limit);

// Generate a value of a type that implements `quickcheck::Arbitrary`
let byte: u8 = fate.roll_arbitrary();
println!("{:?}", byte);
// Output: 0

// Create a `Die` for a type that implements `quickcheck::Arbitrary`
let byte_die = dice::arbitrary();
let bytes_die = dice::vec(byte_die, 1..);
let bytes: Vec<u8> = fate.roll(bytes_die);
println!("{:?}", bytes);
// Output: [1, 4, 4, 2]

许可证

根据您选择的以下任一项获得许可:

任选其一。

贡献

除非您明确表示,否则根据 Apache-2.0 许可证定义的,您有意提交给作品的所有贡献,均应按上述方式双重许可,没有任何额外的条款或条件。

依赖项

~74–370KB