#fixture #proc-macro #fixtures #async-test #test #test-cases

dev rstest

Rust 基础测试框架。它使用过程宏来实现测试框架和表格测试。

33 个版本 (20 个重大更新)

0.22.0 2024 年 8 月 4 日
0.21.0 2024 年 6 月 1 日
0.20.0 2024 年 5 月 30 日
0.18.2 2023 年 8 月 13 日
0.2.2 2018 年 10 月 18 日

#4测试

Download history 208658/week @ 2024-05-03 243741/week @ 2024-05-10 275053/week @ 2024-05-17 258872/week @ 2024-05-24 323399/week @ 2024-05-31 348582/week @ 2024-06-07 314401/week @ 2024-06-14 367148/week @ 2024-06-21 321324/week @ 2024-06-28 318452/week @ 2024-07-05 295770/week @ 2024-07-12 314519/week @ 2024-07-19 315359/week @ 2024-07-26 303184/week @ 2024-08-02 330281/week @ 2024-08-09 314939/week @ 2024-08-16

1,330,707 每月下载量
1,038 包中使用 1,018 个直接使用

MIT/Apache

100KB
1K SLoC

Crate Docs Status Apache 2.0 Licensed MIT Licensed

Rust 的基于固定件的测试框架

简介

rstest 使用过程宏来帮助您编写固定件和基于表格的测试。要使用它,请在您的 Cargo.toml 文件中添加以下行

[dev-dependencies]
rstest = "0.22.0"

功能

  • async-timeout: 为 async 测试提供 timeout (默认启用)
  • crate-name: 使用不同的名称导入 rstest 包 (默认启用)

固定件

核心思想是您可以通过传递它们作为测试参数来注入测试依赖项。在下面的示例中,定义了一个 fixture 并在两个测试中使用它,只需简单地提供它作为参数即可

use rstest::*;

#[fixture]
pub fn fixture() -> u32 { 42 }

#[rstest]
fn should_success(fixture: u32) {
    assert_eq!(fixture, 42);
}

#[rstest]
fn should_fail(fixture: u32) {
    assert_ne!(fixture, 42);
}

参数化

您还可以通过其他方式注入值。例如,您可以通过为每个情况提供注入的值来创建一组测试:rstest 将为每个情况生成一个独立的测试。

use rstest::rstest;

#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn fibonacci_test(#[case] input: u32, #[case] expected: u32) {
    assert_eq!(expected, fibonacci(input))
}

在这种情况下,运行 cargo test 执行五个测试

running 5 tests
test fibonacci_test::case_1 ... ok
test fibonacci_test::case_2 ... ok
test fibonacci_test::case_3 ... ok
test fibonacci_test::case_4 ... ok
test fibonacci_test::case_5 ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

如果您只需为需要运行测试的一组值提供值,则可以使用 #[values) 属性参数

use rstest::rstest;

#[rstest]
fn should_be_invalid(
    #[values(None, Some(""), Some("    "))]
    value: Option<&str>
) {
    assert!(!valid(value))
}

或通过使用 值的列表 为一些变量创建一个 矩阵 测试,这将生成所有值的笛卡尔积。

在更多测试中使用参数化定义

如果您需要为多个测试使用同一个测试列表,可以使用rstest_reuse crate。使用这个辅助crate,您可以在任何地方定义一个模板并使用它。

use rstest::rstest;
use rstest_reuse::{self, *};

#[template]
#[rstest]
#[case(2, 2)]
#[case(4/2, 2)]
fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}

#[apply(two_simple_cases)]
fn it_works(#[case] a: u32, #[case] b: u32) {
    assert!(a == b);
}

有关详细信息,请参阅rstest_reuse

特性标记的案例

如果您希望某些测试案例只在启用特定特性时存在,请使用#[cfg_attr(feature =, case())]

use rstest::rstest;

#[rstest]
#[case(2, 2)]
#[cfg_attr(feature = "frac", case(4/2, 2))]
#[case(4/2, 2)]
fn it_works(#[case] a: u32, #[case] b: u32) {
    assert!(a == b);
}

这同样适用于rstest_reuse

魔法转换

如果您需要一个实现了FromStr()特质的值,可以使用一个字面字符串来构建它

# use rstest::rstest;
# use std::net::SocketAddr;
#[rstest]
#[case("1.2.3.4:8080", 8080)]
#[case("127.0.0.1:9000", 9000)]
fn check_port(#[case] addr: SocketAddr, #[case] expected: u16) {
    assert_eq!(expected, addr.port());
}

您也可以在值列表和测试用例默认值中使用此功能。

异步

rstest提供了开箱即用的async支持。只需将您的测试函数标记为async,并使用#[async-std::test]来注解它。这个特性对于使用整洁语法构建异步参数化测试非常有用

use rstest::*;

#[rstest]
#[case(5, 2, 3)]
#[should_panic]
#[case(42, 40, 1)]
async fn my_async_test(#[case] expected: u32, #[case] a: u32, #[case] b: u32) {
    assert_eq!(expected, async_sum(a, b).await);
}

目前,开箱即支持的是async-std。但如果您需要使用其他提供自己测试属性的运行时(例如,tokio::testactix_rt::test),您可以在您的async测试中使用它,就像在注入测试属性中描述的那样。

要使用此功能,您需要启用attributes在您的Cargo.toml中的async-std功能列表。

async-std = { version = "1.5", features = ["attributes"] }

如果您的测试输入是一个异步值(测试用例或测试参数),您可以使用#[future]属性来移除impl Future<Output = T>样板,并直接使用T

use rstest::*;
#[fixture]
async fn base() -> u32 { 42 }

#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
async fn my_async_test(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
    assert_eq!(expected, base.await / div.await);
}

正如你所注意到的,你应该使用 .await 等待所有 future 值,这有时可能非常无聊。在这种情况下,你可以使用 #[future(awt)]awaiting 一个输入或使用 #[awt] 属性来全局 .await 所有你的 future 输入。之前的代码可以简化如下

use rstest::*;
# #[fixture]
# async fn base() -> u32 { 42 }
#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
#[awt]
async fn global(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
    assert_eq!(expected, base / div);
}
#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
async fn single(#[future] base: u32, #[case] expected: u32, #[future(awt)] #[case] div: u32) {
    assert_eq!(expected, base.await / div);
}

文件路径作为输入参数

如果你需要为给定位置的每个文件创建一个测试,可以使用 #[files("glob 路径语法")] 属性来生成满足给定 glob 路径的每个文件的测试。

#[rstest]
fn for_each_file(#[files("src/**/*.rs")] #[exclude("test")] path: PathBuf) {
    assert!(check_file(&path))
}

默认行为是忽略以 "." 开头的文件,但你可以通过使用 #[include_dot_files] 属性来修改这一点。可以在同一变量上多次使用 files 属性,并且你还可以使用 #[exclude("regex")] 属性来创建一些自定义排除规则,过滤掉所有符合正则表达式的路径。

默认超时时间

你可以使用环境变量 RSTEST_TIMEOUT 为测试设置默认超时时间。该值以秒为单位,在测试编译时进行评估。

使用 #[timeout()]

你可以使用 #[timeout(<duration>)] 属性为你的测试定义执行超时时间。超时对同步和异步测试都适用,且与运行时不相关。 #[timeout(<duration>)] 接收一个表达式,该表达式应该返回一个 std::time::Duration。以下是一个简单的异步示例

use rstest::*;
use std::time::Duration;

async fn delayed_sum(a: u32, b: u32,delay: Duration) -> u32 {
    async_std::task::sleep(delay).await;
    a + b
}

#[rstest]
#[timeout(Duration::from_millis(80))]
async fn single_pass() {
    assert_eq!(4, delayed_sum(2, 2, ms(10)).await);
}

在这种情况下,测试通过,因为延迟仅为10毫秒,超时为80毫秒。

您可以将 timeout 属性像其他任何属性一样用于测试中,并且可以覆盖组超时以使用特定案例的超时。在下面的示例中,我们有 3 个测试,其中第一个和第三个使用 100 毫秒,但第二个使用 10 毫秒。在这个示例中另一个有价值的点是使用表达式来计算持续时间。

fn ms(ms: u32) -> Duration {
    Duration::from_millis(ms.into())
}

#[rstest]
#[case::pass(ms(1), 4)]
#[timeout(ms(10))]
#[case::fail_timeout(ms(60), 4)]
#[case::fail_value(ms(1), 5)]
#[timeout(ms(100))]
async fn group_one_timeout_override(#[case] delay: Duration, #[case] expected: u32) {
    assert_eq!(expected, delayed_sum(2, 2, delay).await);
}

如果您想在 async 测试中使用 timeout,您需要使用 async-timeout 功能(默认启用)。

注入测试属性

如果您想在测试中使用其他 test 属性,您只需在测试函数的属性中指定它即可。例如,如果您想使用 actix_rt::test 属性测试一些异步函数,您只需写下:

use rstest::*;
use actix_rt;
use std::future::Future;

#[rstest]
#[case(2, async { 4 })]
#[case(21, async { 42 })]
#[actix_rt::test]
async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) {
    assert_eq!(2 * a, result.await);
}

只有以 test 结尾的属性(最后一个路径段)可以注入。

使用 #[once] 固定值

如果您需要为所有测试只初始化一次的固定值,您可以使用 #[once] 属性。 rstest 只调用一次您的固定值函数,并返回对您的函数结果的引用到所有测试中

#[fixture]
#[once]
fn once_fixture() -> i32 { 42 }

#[rstest]
fn single(once_fixture: &i32) {
    // All tests that use once_fixture will share the same reference to once_fixture() 
    // function result.
    assert_eq!(&42, once_fixture)
}

局部生命周期和 #[by_ref] 属性

在某些情况下,您可能希望为测试的一些参数使用局部生命周期。在这些情况下,您可以使用 #[by_ref] 属性,然后使用引用而不是值。

enum E<'a> {
    A(bool),
    B(&'a Cell<E<'a>>),
}

fn make_e_from_bool<'a>(_bump: &'a (), b: bool) -> E<'a> {
    E::A(b)
}

#[fixture]
fn bump() -> () {}
 
#[rstest]
#[case(true, E::A(true))]
fn it_works<'a>(#[by_ref] bump: &'a (), #[case] b: bool, #[case] expected: E<'a>) {
    let actual = make_e_from_bool(&bump, b);
    assert_eq!(actual, expected);
}

您可以为测试的所有参数使用 #[by_ref] 属性,而不仅仅是固定值,还包括案例、值和文件。

完整示例

所有这些功能都可以与固定值变量、固定案例和一系列值混合使用。例如,您可能需要两个测试案例来测试恐慌,一个用于已登录用户,另一个用于访客用户。

use rstest::*;

#[fixture]
fn repository() -> InMemoryRepository {
    let mut r = InMemoryRepository::default();
    // fill repository with some data
    r
}

#[fixture]
fn alice() -> User {
    User::logged("Alice", "2001-10-04", "London", "UK")
}

#[rstest]
#[case::authorized_user(alice())] // We can use `fixture` also as standard function
#[case::guest(User::Guest)]   // We can give a name to every case : `guest` in this case
                              // and `authorized_user`
#[should_panic(expected = "Invalid query error")] // We would test a panic
fn should_be_invalid_query_error(
    repository: impl Repository,
    #[case] user: User,
    #[values("     ", "^%$some#@invalid!chars", ".n.o.d.o.t.s.")] query: &str,
) {
    repository.find_items(&user, query).unwrap();
}

此示例将生成正好 6 个测试,按 2 个不同的案例分组

running 6 tests
test should_be_invalid_query_error::case_1_authorized_user::query_1_____ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_2_____someinvalid_chars__ - should panic ... ok
test should_be_invalid_query_error::case_1_authorized_user::query_2_____someinvalid_chars__ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_3____n_o_d_o_t_s___ - should panic ... ok
test should_be_invalid_query_error::case_1_authorized_user::query_3____n_o_d_o_t_s___ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_1_____ - should panic ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

注意,值的名称尝试将输入表达式转换为 Rust 有效的标识符名称,以帮助您找到哪些测试失败。

更多

就这么多吗?还不完全是!

固定值可以被另一个固定值注入,并且可以仅使用其一些参数调用它们。

#[fixture]
fn user(#[default("Alice")] name: &str, #[default(22)] age: u8) -> User {
    User::new(name, age)
}

#[rstest]
fn is_alice(user: User) {
    assert_eq!(user.name(), "Alice")
}

#[rstest]
fn is_22(user: User) {
    assert_eq!(user.age(), 22)
}

#[rstest]
fn is_bob(#[with("Bob")] user: User) {
    assert_eq!(user.name(), "Bob")
}

#[rstest]
fn is_42(#[with("", 42)] user: User) {
    assert_eq!(user.age(), 42)
}

正如您所注意到的,您可以提供默认值,而无需固定值定义它。

最后,如果您需要跟踪输入值,只需将 trace 属性添加到您的测试中即可启用所有输入变量的转储。

#[rstest]
#[case(42, "FortyTwo", ("minus twelve", -12))]
#[case(24, "TwentyFour", ("minus twentyfour", -24))]
#[trace] //This attribute enable tracing
fn should_fail(#[case] number: u32, #[case] name: &str, #[case] tuple: (&str, i32)) {
    assert!(false); // <- stdout come out just for failed tests
}
running 2 tests
test should_fail::case_1 ... FAILED
test should_fail::case_2 ... FAILED

failures:

---- should_fail::case_1 stdout ----
------------ TEST ARGUMENTS ------------
number = 42
name = "FortyTwo"
tuple = ("minus twelve", -12)
-------------- TEST START --------------
thread 'should_fail::case_1' panicked at 'assertion failed: false', src/main.rs:64:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

---- should_fail::case_2 stdout ----
------------ TEST ARGUMENTS ------------
number = 24
name = "TwentyFour"
tuple = ("minus twentyfour", -24)
-------------- TEST START --------------
thread 'should_fail::case_2' panicked at 'assertion failed: false', src/main.rs:64:5


failures:
    should_fail::case_1
    should_fail::case_2

test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out

如果有一个或多个变量没有实现 Debug 特性,则会引发错误,但也可以使用 #[notrace] 参数属性来排除变量。

您可以在 文档 中了解更多信息,并在 tests/resources 目录中找到更多示例。

Rust 版本兼容性

支持的最小Rust版本是1.67.1。

变更日志

请参阅CHANGELOG.md

许可证

在以下许可证之一下授权:

依赖项

~2.6–4.5MB
~80K SLoC