繁体   English   中英

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

[英]How can I create parameterized tests in 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);
}

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

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

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 的单独测试。

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

use rstest::rstest;

#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[case(5, 5)]
#[case(6, 8)]
fn fibonacci_test(#[case] input: u32, #[case] 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 fibonacci_test::case_1 ... ok
test fibonacci_test::case_2 ... ok
test fibonacci_test::case_3 ... ok
test fibonacci_test::case_5 ... ok
test fibonacci_test::case_6 ... ok
test fibonacci_test::case_4 ... ok
test fibonacci_test::case_7 ... ok

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

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

语法简单明了,如果需要,您可以使用任何 Rust 表达式作为case参数中的值。

rstest还支持泛型和pytest的固定装置。


不要忘记将rstest添加到Cargo.toml中的dev-dependencies项。

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

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);
}

我从这个 Quickcheck 教程中进行了 Fibonacci 测试。


PS 当然,即使没有宏和快速检查,您仍然可以在测试中包含参数。 “把事情简单化”。

#[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);
    }
}

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

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

货物.toml

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

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

建造者.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

无需使用任何额外的包,您可以这样做,因为您可以编写返回 Result 类型的测试

#[cfg(test)]
mod tests {
    fn test_add_case(a: i32, b: i32, expected: i32) -> Result<(), String> {
        let result = a + b;
        if result != expected {
            Err(format!(
                "{} + {} result: {}, expected: {}",
                a, b, result, expected
            ))
        } else {
            Ok(())
        }
    }

    #[test]
    fn test_add() -> Result<(), String> {
        [(2, 2, 4), (1, 4, 5), (1, -1, 0), (4, 2, 0)]
            .iter()
            .try_for_each(|(a, b, expected)| test_add_case(*a, *b, *expected))?;

        Ok(())
    }
}

您甚至会收到一条很好的错误消息:

    ---- tests::test_add stdout ----
Error: "4 + 2 result: 6, expected: 0"
thread 'tests::test_add' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/test/src/lib.rs:194:5

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

例子:

#[test_case("some")]
#[test_case("other")]
fn works_correctly(arg: &str) {
    assert!(arg.len() > 0)
}

编辑:这现在在crates.io上作为parameterized_test::create.{...} - 添加parameterized_test = "0.2.0"到您的Cargo.toml文件。


基于Chris Morgan 的回答,这里有一个递归宏来创建参数化测试( 操场):

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! ,尽管它将测试分组到一个模块中,以便它们可以共享一个前缀(建议在此处)。 此示例生成两个测试函数, 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 宏,所以非常欢迎反馈和建议。

借鉴上面Chris Morgan 的精彩回答,我在下面提供了我的使用方法。 除了较小的重构之外,我还添加了测试评估器 function 的要求,它现在概括了宏。 output 也很不错,我的 VS Code 设置会自动扩展测试用例,以便可以从编辑器中单独调用它们。 无论如何,由于label成为测试 function 名称, cargo test确实允许轻松进行测试选择,如cargo test length_

macro_rules! test_cases {
    ($($label:ident: $evaluator:ident $case:expr,)*) => {
    $(
        #[test]
        fn $label() {
            let (expected, input) = $case;
            assert_eq!(expected, $evaluator(input));
        }
    )*
    }
}

fn get_len(s: &str) -> usize {
    s.len()
}

test_cases! {
    length_0: get_len (0, ""), //comments are permitted
    length_1: get_len (2, "AB"), 
    length_2: get_len (9, "123456789"),
    length_3: get_len (14, "not 14 long"),
}

Output...

running 4 tests
test length_0 ... ok
test length_1 ... ok
test length_2 ... ok
test length_3 ... FAILED

failures:

---- length_3 stdout ----
thread 'length_3' panicked at 'assertion failed: `(left == right)`
  left: `14`,
 right: `11`', src/lib.rs:17:1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    length_3

test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM