[英]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_0
、 fib_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::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);
}
我从这个 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::one
和even::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.