如何在Rust中创建参数化测试?

时间:2016-01-07 18:39:08

标签: unit-testing testing rust

我想编写依赖于参数的测试用例。我的测试用例应该针对每个参数执行,我想看看每个参数是成功还是失败。

我过去常常在Java中写这样的东西:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {     
                 { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }  
           });
    }

    private int fInput;

    private int fExpected;

    public FibonacciTest(int input, int expected) {
        fInput= input;
        fExpected= expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}

如何实现与Rust类似的功能?简单的测试用例工作正常,但有些情况下还不够。

#[test]
fn it_works() {
    assert!(true);
}

注意:我希望参数尽可能灵活,例如:从文件中读取它们,或者使用某个目录中的所有文件作为输入等。因此硬编码的宏可能还不够。

6 个答案:

答案 0 :(得分:22)

内置测试框架不支持此功能;最常用的方法是使用宏为每个案例生成一个测试,如下所示:

macro_rules! fib_tests {
    ($($name:ident: $value:expr,)*) => {
    $(
        #[test]
        fn $name() {
            let (input, expected) = $value;
            assert_eq!(expected, fib(input));
        }
    )*
    }
}

fib_tests! {
    fib_0: (0, 0),
    fib_1: (1, 1),
    fib_2: (2, 1),
    fib_3: (3, 2),
    fib_4: (4, 3),
    fib_5: (5, 5),
    fib_6: (6, 8),
}

这会产生名称为fib_0fib_1&c;

的个别测试

答案 1 :(得分:7)

可能不完全是你要求的,但是通过TestResult::discard使用quickcheck,您可以使用随机生成的输入的子集来测试函数。

extern crate quickcheck;

use quickcheck::{TestResult, quickcheck};

fn fib(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

fn main() {
    fn prop(n: u32) -> TestResult {
        if n > 6 {
            TestResult::discard()
        } else {
            let x = fib(n);
            let y = fib(n + 1);
            let z = fib(n + 2);
            let ow_is_ow = n != 0 || x == 0;
            let one_is_one = n != 1 || x == 1;
            TestResult::from_bool(x + y == z && ow_is_ow && one_is_one)
        }
    }
    quickcheck(prop as fn(u32) -> TestResult);
}

我从this Quickcheck tutorial进行了斐波纳契测试。

P.S。当然,即使没有宏和快速检查,您仍然可以在测试中包含参数。 “保持简单”。

#[test]
fn test_fib() {
    for &(x, y) in [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 5), (6, 8)].iter() {
        assert_eq!(fib(x), y);
    }
}

答案 2 :(得分:4)

可以使用a build script基于任意复杂的参数和构建时已知的任何信息(包括可以从文件加载的任何信息)构建测试。

我们告诉Cargo构建脚本在哪里:

<强> Cargo.toml

[package]
name = "test"
version = "0.1.0"
build = "build.rs"

在构建脚本中,我们生成测试逻辑并使用环境变量OUT_DIR将其放在一个文件中:

<强> build.rs

fn main() {
    let out_dir = std::env::var("OUT_DIR").unwrap();
    let destination = std::path::Path::new(&out_dir).join("test.rs");
    let mut f = std::fs::File::create(&destination).unwrap();

    let params = &["abc", "fooboo"];
    for p in params {
        use std::io::Write;
        write!(
            f,
            "
#[test]
fn {name}() {{
    assert!(true);
}}",
            name = p
        ).unwrap();
    }
}

最后,我们在tests目录中创建一个包含生成文件代码的文件。

<强>测试/ generated_test.rs

include!(concat!(env!("OUT_DIR"), "/test.rs"));

那就是它。让我们验证测试是否已运行:

$ cargo test
   Compiling test v0.1.0 (...)
    Finished debug [unoptimized + debuginfo] target(s) in 0.26 secs
     Running target/debug/deps/generated_test-ce82d068f4ceb10d

running 2 tests
test abc ... ok
test fooboo ... ok

答案 3 :(得分:2)

我的rstest crate模仿pytest语法,并提供了很大的灵活性。斐波那契示例可以非常简洁:

#[cfg(test)]
extern crate rstest;

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

    use rstest::rstest_parametrize;

    #[rstest_parametrize(input, expected,
    case(0, 0),
    case(1, 1),
    case(2, 1),
    case(3, 2),
    case(4, 3),
    case(5, 5),
    case(6, 8)
    )]
    fn fibonacci_test(input: u32, expected: u32) {
        assert_eq!(expected, fibonacci(input))
    }
}

pub fn fibonacci(input: u32) -> u32 {
    match input {
        0 => 0,
        1 => 1,
        n => fibonacci(n - 2) + fibonacci(n - 1)
    }
}

输出:

/home/michele/.cargo/bin/cargo test
   Compiling fib_test v0.1.0 (file:///home/michele/learning/rust/fib_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.92s
     Running target/debug/deps/fib_test-56ca7b46190fda35

running 7 tests
test test::fibonacci_test_case_0 ... ok
test test::fibonacci_test_case_1 ... ok
test test::fibonacci_test_case_2 ... ok
test test::fibonacci_test_case_4 ... ok
test test::fibonacci_test_case_5 ... ok
test test::fibonacci_test_case_3 ... ok
test test::fibonacci_test_case_6 ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

每个案例都作为一个测试案例运行。

语法简单明了,如果您需要为参数编写野生代码,则可以使用Unwrap("...everything can be valid Rust code...")作为case参数中的值(确定{ {1}}不是一个不错的选择,但我打算对其进行更改。

Unwrap还支持泛型和类似rstest的灯具。


不要忘记将pytest添加到rstest中的dev-dependencies

答案 4 :(得分:1)

Chris Morgan’s answer为基础,这是一个用于创建参数化测试(playground)的递归宏:

macro_rules! parameterized_test {
    ($name:ident, $args:pat, $body:tt) => {
        with_dollar_sign! {
            ($d:tt) => {
                macro_rules! $name {
                    ($d($d pname:ident: $d values:expr,)*) => {
                        mod $name {
                            use super::*;
                            $d(
                                #[test]
                                fn $d pname() {
                                    let $args = $d values;
                                    $body
                                }
                            )*
                        }}}}}}}

您可以像这样使用它:

parameterized_test!{ even, n, { assert_eq!(n % 2, 0); } }
even! {
    one: 1,
    two: 2,
}

parameterized_test!定义了一个新的宏(even!),该宏将创建带有一个参数(n)并调用assert_eq!(n % 2, 0);的参数化测试。

even!的工作原理基本上类似于Chris的fib_tests!,尽管它将测试分组到一个模块中,以便它们可以共享前缀(建议here)。此示例产生两个测试函数even::oneeven::two

同一语法适用于多个参数:

parameterized_test!{equal, (actual, expected), {
    assert_eq!(actual, expected); 
}}
equal! {
    same: (1, 1),
    different: (2, 3),
}

上面用于实质上逃避内部宏中美元符号的with_dollar_sign!宏来自@durka

macro_rules! with_dollar_sign {
    ($($body:tt)*) => {
        macro_rules! __with_dollar_sign { $($body)* }
        __with_dollar_sign!($);
    }
}

我以前没有写过很多Rust宏,因此非常欢迎您提供反馈和建议。

答案 5 :(得分:1)

使用https://github.com/frondeus/test-case条板箱。

示例:

50