如何在 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);
}

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

16832 次浏览

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

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

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

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

我做了 这个快速检查教程的斐波那契检验。


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

#[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 构建脚本在哪里:

货物,汤姆

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

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

Test/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

我的 rstest箱子模仿 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的装置。


不要忘记在 Cargo.toml中将 rstest加到 dev-dependencies

编辑: 这是现在的 板条箱作为 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 宏,所以非常欢迎反馈和建议。

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

例如:

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

不需要使用任何其他包,您可以这样做,因为您可以 编写返回 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