Подтвердить что ты не робот

Как я могу создать параметризованные тесты в 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);
}

Примечание. Я хочу, чтобы параметры были максимально гибкими, например: читайте их из файла или используйте все файлы из определенного каталога в качестве входных данных и т.д. Таким образом, жестко запрограммированный макрос может оказаться недостаточным.

4b9b3361

Ответ 1

Встроенная среда тестирования не поддерживает это; наиболее распространенный подход - генерировать тест для каждого случая с использованием макросов, например:

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.

Ответ 2

Возможно, не совсем то, что вы просили, но с помощью 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);
}

Я прошел тест Фибоначчи из этого руководства по быстрой проверке.


Постскриптум И, конечно же, даже без макросов и быстрой проверки вы все равно можете включить параметры в тест. "Сохраняй это простым".

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

Ответ 3

С помощью сценария сборки можно создавать тесты на основе произвольно сложных параметров и любой информации, известной во время сборки (включая все, что вы можете загрузить из файла).

Мы сообщаем Cargo, где находится скрипт сборки:

Cargo.toml

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

В скрипте сборки мы генерируем нашу тестовую логику и OUT_DIR ее в файл, используя переменную среды 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();
    }
}

Наконец, мы создаем файл в нашем каталоге тестов, который включает код сгенерированного файла.

тесты/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

Ответ 4

Моя rstest имитирует синтаксис 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 (хорошо, Unwrap не является отличный выбор но я планирую его поменять).

rstest также поддерживает дженерики и pytest -like.


Не забудьте добавить rstest к dev-dependencies в Cargo.toml.

Ответ 5

Построение ответа Криса Моргана, вот рекурсивный макрос для создания параметризованных тестов (игровая площадка):

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! затем работает по существу как fib_tests! Криса 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 раньше, поэтому отзывы и предложения приветствуются.

Ответ 6

Используйте https://github.com/frondeus/test-case ящик.

Пример:

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