#api-testing #minimalist #property-based #run

开发工具 arbtest

基于任意值的极简属性测试库

7 个版本

0.3.1 2024年2月22日
0.3.0 2024年2月21日
0.2.0 2022年10月31日
0.1.2 2022年4月10日

#52 in 测试

Download history 1839/week @ 2024-03-13 1147/week @ 2024-03-20 815/week @ 2024-03-27 913/week @ 2024-04-03 879/week @ 2024-04-10 562/week @ 2024-04-17 678/week @ 2024-04-24 732/week @ 2024-05-01 591/week @ 2024-05-08 797/week @ 2024-05-15 933/week @ 2024-05-22 1935/week @ 2024-05-29 2171/week @ 2024-06-05 4702/week @ 2024-06-12 533/week @ 2024-06-19 716/week @ 2024-06-26

9,084 每月下载次数
用于 5 crates

MIT/Apache

21KB
244

arbtest

一个功能强大、API 简小、实现精简的属性测试库。

use arbtest::arbtest;

#[test]
fn all_numbers_are_even() {
    arbtest(|u| {
        let number: u32 = u.arbitrary()?;
        assert!(number % 2 == 0);
        Ok(())
    });
}

lib.rs:

一个功能强大、API 简小、实现精简的属性测试库。

use arbtest::arbtest;

#[test]
fn all_numbers_are_even() {
    arbtest(|u| {
        let number: u32 = u.arbitrary()?;
        assert!(number % 2 == 0);
        Ok(())
    });
}

特性

  • 单函数公共 API,
  • 无宏,
  • 自动最小化,
  • 时间预算,
  • 与 fuzzer 兼容的测试。

入口点是 arbtest 函数。它接受一个参数 —— 要测试的属性。属性是一个具有以下签名的函数

/// Panics if the property does not hold.
fn property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()>

u 参数是来自 arbitrary crate 的有限随机数生成器。您可以使用 u 生成伪随机结构化数据

let ints: Vec<u32> = u.arbitrary()?;
let fruit: &str = u.choose(&["apple", "banana", "cherimoya"])?;

或者使用 arbitrary crate 的 derive 功能来自动生成任意类型

#[derive(arbitrary::Arbitrary)]
struct Color { r: u8, g: u8, b: u8 }

let random_color = u.arbitrary::<Color>()?;

属性函数应使用随机生成的数据来断言实现的某些有趣行为,这对于 任何 值都应成立。例如,将颜色转换为字符串然后解析它应得到相同的颜色

#[test]
fn parse_is_display_inverted() {
    arbtest(|u| {
        let c1: Color = u.arbitrary();
        let c2: Color = c1.to_string().parse().unwrap();
        assert_eq!(c1, c2);
        Ok(())
    })
}

在您提供了属性函数后,arbtest 会在循环中反复运行它,传递越来越多的 arbitrary::Unstructured 字节,直到属性崩溃。在失败时,会打印出一个种子。该种子可以用于确定性地重放失败。

thread 'all_numbers_are_even' panicked at src/lib.rs:116:9:
assertion failed: number % 2 == 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

arbtest failed!
    Seed: 0xa88e234400000020

通过返回的 ArbTest 对象上的构建器式 API,可以获得更多功能。

时间预算

arbtest(property).budget_ms(1_000);
arbtest(property).budget(Duration::from_secs(1));

budget 函数控制搜索循环运行多长时间,默认为 100 毫秒。此默认值可以通过 ARBTEST_BUDGET_MS 环境变量来覆盖。

大小约束

arbtest(property)
    .size_min(1 << 4)
    .size_max(1 << 16);

在内部,arbitrary::Unstructured 只是一个 &[u8] —— 随机字节的切片。这个切片的长度决定了测试可以使用的随机性程度。较短的切片包含较少的熵,导致测试用例更简单。

size_minsize_max 参数控制这个切片的长度:在寻找失败时,arbtest 会从 size_min 逐步增加到 size_max

注意,在尝试最小化已知失败时,arbtest 将尝试比 size_min 更小的值。

重放和最小化

arbtest(property).seed(0x92);
arbtest(property).seed(0x92).minimize();

当指定了 seed 时,arbtest 使用种子生成一个固定的 Unstructured 并运行属性函数一次。这有助于在通过搜索找到失败种子后调试测试失败。

如果除了 seed 之外还设置了 minimize,那么 arbtest 将尝试找到一个更小的种子,该种子仍然可以触发失败。你可以使用 budget 来控制最小化运行的时间。

代码何时运行

arbtest 函数不会立即运行代码。相反,它返回一个可以用来进一步调整行为的 ArbTest 构建器对象。实际的执行是从 ArbTest::drop 触发的。如果你不喜欢在 drop 中崩溃,你可以使用 ArbTest::run 方法显式触发执行

let builder = arbtest(property);
drop(builder); // This line actually runs the tests.

arbtest(property).run(); // Request the run explicitly.

错误

属性失败应通过 panic 报告,例如,使用 assert_eq! 宏。返回一个 Err(arbitrary::Error) 并不表示测试失败,它只是意味着没有足够的熵来完成测试。而不是返回一个 arbitrary::Error,测试可能选择以一种非随机的方式继续。例如,在测试分布式系统时,你可能使用以下模板

while !u.is_empty() && network.has_messages_in_flight() {
    network.drop_and_permute_messages(u);
    network.deliver_next_message();
}
while network.has_messages_in_flight() {
    network.deliver_next_message();
}

导入

推荐导入方式

[dev-dependencies]
arbtest = "0.3"
#[cfg(test)]
mod tests {
    use arbtest::{arbtest, arbitrary};

    fn my_property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()> { Ok(()) }
}

如果你想要 #[derive(Arbitrary)],你需要显式地为 arbitrary crate 添加 Cargo.toml 依赖

[dependencies]
arbitrary = { version = "1", features = ["derive"] }

[dev-dependencies]
arbtest = "0.3"
#[derive(arbitrary::Arbitrary)]
struct Color { r: u8, g: u8, b: u8 }

#[cfg(test)]
mod tests {
    use arbtest::arbtest;

    #[test]
    fn display_parse_identity() {
        arbtest(|u| {
            let c1: Color = u.arbitrary()?;
            let c2: Color = c1.to_string().parse();
            assert_eq!(c1, c2);
            Ok(())
        });
    }
}

请注意,arbitrary 是一个非开发依赖项。这不是严格必需的,但有助于允许下游的crate使用任意值的Color来运行它们的测试。

设计

大部分的重活都是由arbitrary crate来完成的。它的arbitrary::Unstructured是一个出色的抽象,它既适用于覆盖率指导的模糊测试,也适用于自动最小化。也就是说,您可以直接将arbtest属性插入到cargo fuzz中,API完全兼容。

属性函数使用&mut Unstructured作为参数,而不是T: Arbitrary,允许用户强制生成他们想要的任何T。这里的较小好处是实现简单——属性类型不是泛型的。更大的好处是,这个API更具表现力,因为它允许有交互式属性。例如,一个分布式系统的网络模拟不需要提前生成“故障计划”,它可以在测试运行期间使用u来对要丢弃哪些现有的网络数据包做出动态决定!

“种子”是一个u64,按照惯例用十六进制表示。种子的低32位指定了底层Unstructured的长度。高32位是真正的随机种子,它被输入到一个简单的xor-shift中,以生成指定长度的Unstructured

如果您喜欢这个crate,您可能也会喜欢https://github.com/graydon/exhaustigen-rs

依赖项

~92KB