#test-cases #test-framework #fixtures #unit-testing #test #unit-tests

galvanic-test

一个支持参数化测试用例的测试框架,用于设置和撤销测试固定装置/环境。该crate是galvanic的一部分——一个完整的Rust测试框架

7个版本

0.2.0 2018年12月22日
0.1.5 2018年10月17日
0.1.4 2018年5月5日
0.1.3 2017年10月18日
0.1.0 2017年9月28日

#255测试

Download history 654/week @ 2024-03-13 612/week @ 2024-03-20 656/week @ 2024-03-27 510/week @ 2024-04-03 392/week @ 2024-04-10 179/week @ 2024-04-17 256/week @ 2024-04-24 382/week @ 2024-05-01 459/week @ 2024-05-08 547/week @ 2024-05-15 678/week @ 2024-05-22 237/week @ 2024-05-29 328/week @ 2024-06-05 343/week @ 2024-06-12 506/week @ 2024-06-19 296/week @ 2024-06-26

1,498 每月下载量
用于 6 crate

Apache-2.0

30KB
286

Galvanic-test:Rust测试设置更简单

Build Status Crates.io

该crate提供创建测试套件、管理它们的共享依赖项以及编写参数化测试的框架。

  • 支持测试固定装置来处理测试依赖项的设置/撤销
  • 带参数的测试固定装置在测试设置中提供更多灵活性
  • 参数化测试——编写一次测试并在不同场景中验证它
  • 自动将测试固定装置注入到测试中
  • 使用 galvanic-assert 提供更丰富的测试设计——表达参数化测试的更具体属性
  • 测试套件与 galvanic-mock 集成——无需手动 #[use_mocks]
  • 也可以使用其他你选择的基于panic的断言和模拟框架

该crate是 galvanic 的一部分——一个完整的 Rust 测试框架。该框架分为三个部分,因此你可以选择只使用你需要的部分。

Galvanic-test简短介绍

Galvanic-test 简化了测试环境的设置和撤销,并帮助您组织测试。您应该仍然了解关于Rust测试的所有内容。

测试按测试套件组织,这些套件可以是命名的或匿名的。

use galvanic_test::test_suite;

// test suites are only built when a test is executed, e.g., with `cargo test`
test_suite! {
    // for anonymous test suites remove the name directive
    name my_test_suite;

    // suites act as modules and may contain any item
    fn calc(a: i32, b: i32) -> i32 { a*b }

    // instead of `fn`, `test` defines a test item.
    test simple_first_test() {
        assert_eq!(3*2, 6);
    }

    // attributes can usually be applied as for functions, e.g., #[should_panic(expected = "...")] is curently not supported
    #[should_panic]
    test another_test() {
        assert_eq!(calc(3,2), 7);
    }
}

galvanic-test 最强大的部分是测试夹具,用于管理测试环境。测试夹具是一段代码,用于设置测试的特定部分,并确保测试执行完成后将其拆除(即使测试失败)。如果您熟悉 pytest,应该会感到很熟悉。如果您有使用 XUnit-style 框架的经验,例如 JUnit、CPPUnit 等,那么您可以将夹具视为属于一起的不同的 before/after 块。

use galvanic_test::test_suite;

test_suite! {
    use std::fs::{File, remove_file};
    use std::io::prelude::*;

    fixture bogus_number() -> i32 {
        setup(&mut self) {
            42
        }
    }

    fixture input_file(file_name: String, content: String) -> File {
        members {
            file_path: Option<String>
        }
        setup(&mut self) {
            let file_path = format!("/tmp/{}.txt", self.file_name);
            self.file_path = Some(file_path.clone());
            {
                let mut file = File::create(&file_path).expect("Could not create file.");
                file.write_all(self.content.as_bytes()).expect("Could not write input.");
            }
            File::open(&file_path).expect("Could not open file.")
        }
        // tear_down is optional
        tear_down(&self) {
            remove_file(self.file_path.as_ref().unwrap()).expect("Could not delete file.")
        }
    }

    // fixtures are arguments to the tests
    test a_test_using_a_fixture(bogus_number) {
        assert_eq!(21*2, bogus_number.val);
    }

    // fixtures with arguments must receive the required values
    test another_test_using_fixtures(input_file(String::from("my_file"), String::from("The stored number is: 42"))) {
        let mut read_content = String::new();
        input_file.val.read_to_string(&mut read_content).expect("Couldn't read 'my_file'");

        assert_eq!(&read_content, input_file.params.content);
    }
}

测试夹具还使我们能够使用不同的参数化运行相同的测试代码。这可以显著减少我们对具有多个执行路径的复杂代码进行测试所需的工作量。

test_suite! {
    fixture product(x: u32, y: u32) -> u32 {
        params {
            vec![(2,3), (2,4), (1,6), (1,5), (0,100)].into_iter()
        }
        setup(&mut self) {
            self.x * self.y
        }
    }

    test a_parameterised_test_case(product) {
        let wrong_product = (0 .. *product.params.y).fold(0, |p,_| p + product.params.x) - product.params.y%2;
        // fails for (2,3) & (1,5)
        assert_eq!(wrong_product, product.val)
    }
}

文档

Galvanic-test 简化了共享测试环境的设置,即它帮助我们创建和重置测试所需资源的操作。

建议您将 galvanic-test 添加为 Cargo.toml 中的 dev-dependency。请确保使用适当的版本规范。该包遵循语义版本化。

对于 Rust 版本 2018,使用至少 0.2 的版本号。

[dev-dependencies]
galvanic-test = "0.2"

指定依赖项后,我们可以按照以下方式导入 test_suite 宏。

use galvanic_test::test_suite;

当将 galvanic-test 作为 dev-dependency 使用时,请确保 use 语句只能在您的包在启用测试时编译时才能访问,例如,将其包装在带有 #[cfg(test)] 注解的模块中。

Rust 版本在 2018 之前

对于使用 Rust 版本在 2018 之前的包,使用最多 0.1.5 的版本号。

[dev-dependencies]
galvanic-test = "0.1.5"

指定依赖项后,我们可以在 main.rslib.rs 和/或我们的集成测试中的 tests/ 中包含库并启用宏。

#[cfg(test) #[macro_use] extern crate galvanic_test;

当将 galvanic-test 作为 dev-dependency 使用时,请确保任何 galvanic-test 宏只能在测试启用时访问。

创建测试套件以分组测试

在我们开始编写测试之前,我们先看看如何对它们进行分组。测试是组织在测试套件中的。测试套件负责多项事情:

  • 它们创建一个私有模块来分组测试用例和测试夹具。
  • 只有在构建测试时才会构建它们,例如,使用 cargo test
  • 套件中定义的测试夹具可以注入到其测试用例中。
  • 如果启用了 galvanic_mock_integration 功能,则测试套件使用隐式 #[use_mocks] 指令。(nightly

它们有两种类型:匿名命名。要创建匿名测试套件,我们使用 test_suite! 宏。

test_suite! {
    // ...
}

为了更容易定位失败的测试用例,建议命名测试套件。

test_suite! {
    name some_identifer_naming_the_suite;
    // ...
}

请注意,name 指令必须作为套件的第一个元素出现。

在测试套件中编写测试

现在我们已经定义了一个测试套件,我们可以用测试用例填充它。测试用例被定义为 test 项。

test_suite! {
    test my_first_test_case() {
        // ... some assertions
        assert_eq!(1+1, 2);
    }
}

如果我们想要定义一个预期会引发恐慌的测试,我们可以简单地使用 #[should_panic] 属性,或者如果我们需要更精细的控制,可以使用 galvanic-assertassert_that!(..., panics); 宏。

test_suite! {
    #[should_panic]
    test a_panicking_test_case() {
        // ... some failing assertion
        assert_eq!(1+1, 4);
    }
    test a_panicking_test_case_using_galvanic_assert() {
        assert_that(panic("No towels!"), panics);
    }
}

到目前为止,测试用例的行为类似于用 #[test] 注解的函数,就像简单的 Rust 单元测试。不过,定义为一个 test 项的测试用例支持自动注入测试固定值和参数化,正如我们稍后将要看到的。

添加测试固定值以管理测试资源

测试通常依赖于测试环境的某些资源,例如,测试使用的对象、包含输入的文件等。所有这些事情必须在测试开始时创建,在测试结束时销毁。如果我们忘记或搞砸了这些任务中的任何一个,我们就会在测试代码中引入错误,而这些错误实际上并不是测试的核心。

为了保持我们的测试整洁,我们不希望多次编写设置和销毁任务。因此,我们为每个资源编写一个 测试固定值

fixture a_number() -> i32 {
    setup(&mut self) {
        42
    }
    tear_down(&self) {
        println!("Cleaning up ...");
    }
}

每个固定值定义由以下部分组成

  • fixture 关键字
  • 一个 名称:例如,我们的例子中的 a_number
  • 一个类型化参数列表:例如,我们的例子中的
  • 固定值管理的 资源 类型:例如,这里的 i32
  • 一个必需的 setup 块,它接收固定值(self)作为一个可变借用,并必须返回固定值指定类型的资源
  • 一个 可选的 tear_down 块,它接收固定值(self)作为一个不可变借用

要在测试中使用我们新的固定值,它必须在同一 test_suite! 中定义。测试所需的固定值作为测试用例的参数按名称提供。在执行测试之前,调用 setup 方法。然后,它的返回值被包装在一个 FixtureBinding 中,并将绑定注入到测试用例中。然后,可以通过绑定成员 val 访问返回值。

test a_test_using_a_fixture(a_number) {
    assert_eq!(a_number.val, 42);
}

带参数的测试固定值

通常为几个测试设置完全相同的资源是不够的,我们希望对 setup/tear_down 代码进行参数化。我们可以通过为固定值指定参数来实现这一点。

fixture offset_number(offset: i32) -> i32 {
    setup(&mut self) {
        self.offset + 42
    }
    tear_down(&self) {
        println!("Cleaning up a number with offset {} ...", self.offset);
    }
}

然后,这些参数可以作为固定值的成员访问。测试可以指定在请求固定值时所需的参数。传递给固定值的参数可以通过 FixtureBindingparams 成员通过固定值定义中使用的名称在测试用例中访问。

test a_test_using_a_fixture(offset_number(8)) {
    assert_eq!(offset_number.val, 42 + offset_number.params.offset);
}

在 setup() 和 tear_down() 之间共享数据

我们已经看到,固定值参数可以通过 selfsetuptear_down 块中访问。然而,在某些情况下,我们需要在外部输入上依赖某些东西,例如,系统时间、随机数,这些在设置代码和销毁代码中都是必要的,例如,创建唯一的文件名或其他标识符。到目前为止,我们没有(非破坏性的)方法来传输这些信息。

为了解决这个问题,我们可以为我们的测试用例定义成员变量。成员变量可以通过 self 访问,并且始终是 Option 类型,初始化为 None。然后 setup 块可以覆盖成员变量的值(因此它的 &mut self)。

要声明成员变量,我们需要在 setup 块之前放置一个 members 块,并像在 struct 中一样列出我们的变量声明。

fixture offset_number() -> i32 {
    members {
        some_identifier: Option<i32>
    }
    setup(&mut self) {
        self.some_identifier = Some(12)
        42
    }
    tear_down(&self) {
        println!("Cleaning up a fixture with identifier {} ...", self.some_identifier.as_ref().unwrap();
    }
}

编写参数化测试和测试用例

galvanic-test 的一个非常强大的功能是能够参数化测试。参数化测试用例将使用几个不同的测试用例初始化来运行。

首先,我们需要一个接受一个或多个测试用例的测试用例。让我们编写一个测试用例,它通过将 xy 相加 y 次来计算两个数的乘积 (x,y)

test parameterised_test(product) {
    let sum: u32 = (0..product.params.y).fold(0, |a,b| a + product.params.x);
    assert_eq!(sum, product.val);
}

我们想要使用不同的值来测试代码片段的边界情况和等价类。为此,我们创建一个带有参数 xyproduct 测试用例,让 setup 块计算两个数的乘积。为了使测试用例参数化,我们在开头添加一个 params 块。该块必须返回一个 Iterator<R>,其中 R 是测试用例返回值的类型。

fixture product(x: u32, y: u32) -> u32 {
    params {
        vec![(2,3), (1,4), (0,100)].into_iter()
    }
    setup(&mut self) {
        self.x * self.y
    }
}

现在,如果我们运行测试,每个接受 product 测试用例作为参数而不向测试用例提供参数的测试用例将使用 params 块中的值。在参数化之前/之后将执行 setuptear_down 块。

如果测试用例接受多个参数化测试用例,则将评估所有可能的组合(交叉积)。同样,在参数化之前/之后将执行参数化测试用例的所有 setup/tear_down 块。

另一方面,如果您提供了一个参数化测试用例的参数,如下所示,则只需考虑该参数化。

test parameterised_test(product(3,8)) {
    let sum: u32 = (0..product.params.y).fold(0, |a,b| a + product.params.x);
    assert_eq!(sum, 24);
}

错误、#[should_panic] 以及参数化测试用例

让我们看看如果测试失败会发生什么。

test failing_parameterised_test(product) {
    let sum: i32 = (0..*product.params.y).fold(0, |a,b| a + product.params.x);
    assert_eq!(sum, product.val - product.params.x%2)
}

框架将向您显示所有触发错误的参数化,这将使调试更加容易。

...
running 1 test
thread 'test::parameterised_test' panicked at 'assertion failed: `(left == right)`
  left: `4`,
 right: `3`', src/main.rs:17:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
The above error occured with the following parameterisation of the test case:
    product { x: 1, y: 4 }

thread 'test::parameterised_test' panicked at 'Some parameterised test cases failed', src/main.rs:3:0
test test::parameterised_test ... FAILED
...

当将 #[should_panic] 应用于参数化测试用例时请小心。在这种情况下,如果 任何 参数化失败,测试将成功。为了断言所有参数化都失败,建议使用来自 galvanic-assertassert_that!(..., panics),将崩溃处理成常规行为。

进一步地,#[should_panic(expected = "message")] 在带有固定设施的测试中目前不支持,因为测试输出被修改以包含有关失败设施参数化的信息。

启用电化学模拟集成

如果您想使用 galvanic-mock 集成(仅在夜间版本中可用),则添加

#[macro_use] extern crate galvanic_test;
#![feature(proc_macro)]
extern crate galvanic_mock;

并在您的 Cargo.toml 中启用 galvanic_mock_integration 功能

[dev-dependencies]
galvanic-test = { version = "*", features = ["galvanic_mock_integration"] }
galvanic-mock = "*" # replace with the correct version

之后,每个测试套件将自动应用 #[use_mocks] 属性,这样您就可以使用固定设施返回实际的模拟对象。

依赖关系

~0–280KB