unit testing - ¿Cómo puedo crear pruebas parametrizadas en Rust?
unit-testing (4)
Quiero escribir casos de prueba que dependan de parámetros. Mi caso de prueba debe ejecutarse para cada parámetro y quiero ver si tiene éxito o no para cada parámetro.
Estoy acostumbrado a escribir cosas así en 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));
}
}
¿Cómo puedo lograr algo similar con Rust? Los casos de prueba simples funcionan bien, pero hay casos en los que no son suficientes.
#[test]
fn it_works() {
assert!(true);
}
Nota: Quiero que los parámetros sean lo más flexibles posible, por ejemplo: Léalos desde un archivo o use todos los archivos de un directorio determinado como entrada, etc. Por lo tanto, una macro codificada podría no ser suficiente.
El marco de prueba incorporado no admite esto; El enfoque más común utilizado es generar una prueba para cada caso utilizando macros, como este:
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),
}
Esto produce pruebas individuales con los nombres fib_0
, fib_1
, & c.
Es posible construir pruebas basadas en parámetros arbitrariamente complejos y cualquier información conocida en el momento de la compilación (incluido cualquier cosa que pueda cargar desde un archivo) con un script de compilación .
Le decimos a Cargo donde el script de compilación es:
Cargo.toml
[package]
name = "test"
version = "0.1.0"
build = "build.rs"
En el script de compilación, generamos nuestra lógica de prueba y la colocamos en un archivo utilizando la variable de entorno OUT_DIR
:
construir.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();
}
}
Finalmente, creamos un archivo en nuestro directorio de pruebas que incluye el código del archivo generado.
pruebas / tests_ generados.rs
include!(concat!(env!("OUT_DIR"), "/test.rs"));
Eso es. Vamos a verificar que las pruebas se ejecutan:
$ 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
Mi rstest
cajón imita la sintaxis de pytest
y proporciona mucha flexibilidad. Un ejemplo de Fibonacci puede ser muy limpio:
#[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)
}
}
Salida:
/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
Cada caso se ejecuta como un solo caso de prueba.
La sintaxis es simple y clara y si necesita escribir un código salvaje para sus parámetros, simplemente puede usar Unwrap("...everything can be valid Rust code...")
como el valor en el argumento del case
(ok, Unwrap
no es una gran elección pero planeo cambiarlo).
rstest
también es compatible con genéricos y pytest
parecidos a pytest
.
No te olvides de agregar rstest
a dev-dependencies
en Cargo.toml
.
Probablemente no sea exactamente lo que ha pedido, pero al usar TestResult::discard
con una selección quickcheck , puede probar una función con un subconjunto de una entrada generada aleatoriamente.
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);
}
Tomé la prueba de Fibonacci de este tutorial de Quickcheck .
PS Y, por supuesto, incluso sin macros y comprobación rápida, puede incluir los parámetros en la prueba. "Mantenlo simple".
#[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);
}
}